Skip to content

Next Generation Scenes: Core scene system, bsn! macro, Templates#23413

Open
cart wants to merge 12 commits intobevyengine:mainfrom
cart:next-generation-scenes
Open

Next Generation Scenes: Core scene system, bsn! macro, Templates#23413
cart wants to merge 12 commits intobevyengine:mainfrom
cart:next-generation-scenes

Conversation

@cart
Copy link
Member

@cart cart commented Mar 18, 2026

After much iteration, designing and collaborating, it is finally time to land a baseline featureset of Bevy's Next Generation Scene system, often known by its new scene format name ... BSN (Bevy Scene Notation).

This PR adds the following:

  • The new scene system: The core in-memory traits, asset types, and functionality for Bevy's new scene system. Spawn Scenes and SceneLists. Inherit from other scenes. Patch component fields. Depend on assets before loading as scene. Resolve Entity references throughout your scene.
  • The bsn! and bsn_list! macros: Define Bevy scenes in your code using a new ergonomic Rust-ey syntax, which plays nicely with Rust Analyzer and supports autocomplete, go-to definition, semantic highlighting, and doc hover.
  • Template / GetTemplate: construct types (ex: Components) from a "template context", which includes access to the current entity and access to the World. This is a foundational piece of the scene system.

Note that this does not include a loader for the BSN asset format, which will be added in a future PR. See the "Whats Next?" section for a roadmap of the future.

Part of #23030

Review Etiquette

This is a big PR. Please use threaded comments everywhere, not top level comments. Even if what you have to say is not anchored in code, find a line to leave your comment on.

Overview

This is a reasonably comprehensive conceptual overview / feature list. This uses a "bottom up" approach to illustrate concepts, as they build on each other. If you just want to see what BSN looks like, scroll down a bit!

Templates

Template is a simple trait implemented for "template types", which when passed an entity/world context, can produce an output type such as a Component or Bundle:

pub trait Template {
    type Output;
    fn build_template(&mut self, context: &mut TemplateContext) -> Result<Self::Output>;
}

Template is the cornerstone of the new scene system. It allows us to define types (and hierarchies) that require no World context to define, but can use the World to produce the final runtime state. Templates are notably:

  • Repeatable: Building a Template does not consume it. This allows us to reuse "baked" scenes / avoid rebuilding scenes each time we want to spawn one. If a Template produces a value this often means some form of cloning is required.
  • Clone-able: Templates can be duplicated via Template::clone_template, enabling scenes to be duplicated, supporting copy-on-write behaviors, etc.
  • Serializable: Templates are intended to be easily serialized and deserialized, as they are typically composed of raw data.

The poster-child for templates is the asset Handle<T>. We now have a HandleTemplate<T>, which wraps an AssetPath. This can be used to load the requested asset and produce a strong Handle for it.

impl<T: Asset> Template for HandleTemplate<T> {
    type Output = Handle<T>;
    fn build_template(&mut self, context: &mut TemplateContext) -> Result<Handle<T>> {
        Ok(context.resource::<AssetServer>().load(&self.path))
    }
}

Types that have a "canonical" Template can implement the GetTemplate trait, allowing us to correlate to something's Template in the type system.

impl<T: Asset> GetTemplate for Handle<T> {
    type Template = HandleTemplate<T>;
}

This is where things start to get interesting. GetTemplate can be derived for types whose fields also implement GetTemplate:

#[derive(Component, GetTemplate)]
struct Sprite {
  image: Handle<Image>,
}

Internally this produces the following:

#[derive(Template)]
struct SpriteTemplate {
  image: HandleTemplate<Image>,
}

impl GetTemplate for Sprite {
    type Template = SpriteTemplate;
}

Another common use case for templates is Entity. With templates we can resolve an identifier of an entity in a scene to the final Entity it points to (for example: an entity path or an "entity reference" ... this will be described in detail later).

Both Template and GetTemplate are blanket-implemented for any type that implements both Clone and Default. This means that most types are automatically usable as templates. Neat!

impl<T: Clone + Default> Template for T {
    type Output = T;

    fn build_template(&mut self, context: &mut TemplateContext) -> Result<Self::Output> {
        Ok(self.clone())
    }
}

impl<T: Clone + Default> GetTemplate for T {
    type Template = T;
}

It is best to think of GetTemplate as an alternative to Default for types that require world/spawn context to instantiate. Note that because of the blanket impl, you cannot implement GetTemplate, Default, and Clone together on the same type, as it would result in two conflicting GetTemplate 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).

Scenes

Templates on their own already check many of the boxes we need for a scene system, but they aren't enough on their own. We want to define scenes as patches of Templates. This allows scenes to inherit from / write on top of other scenes without overwriting fields set in the inherited scene. We want to be able to "resolve" scenes to a final group of templates.

This is where the Scene trait comes in:

pub trait Scene: Send + Sync + 'static {
    fn resolve(&self, context: &mut ResolveContext, scene: &mut ResolvedScene) -> Result<(), ResolveSceneError>;
    fn register_dependencies(&self, _dependencies: &mut Vec<AssetPath<'static>>);
}

The ResolvedScene is a collection of "final" Template instances which can be applied to an entity. Scene::resolve applies the Scene as a "patch" on top of the final ResolvedScene. It stores a flat list of templates to be applied to the top-level entity and typed lists of related entities (ex: Children, Observers, etc), which each have their own ResolvedScene. Scenes are free to modify these lists, but in most cases they should probably just be pushing to the back of them. ResolvedScene can handle both repeated and unique instances of a template of a given type, depending on the context.

Scene::register_dependencies allows the Scene to register whatever asset dependencies it needs to perform Scene::resolve. The scene system will ensure Scene::resolve is not called until all of the dependencies have loaded.

Scene is always one top level / root entity. For "lists of scenes" (such as a list of related entities), we have the SceneList trait, which can be used in any place where zero to many scenes are expected. These are separate traits for logical reasons: world.spawn() is a "single entity" action, scene inheritance only makes sense when both scenes are single roots, etc.

Template Patches

The TemplatePatch type implements Scene, and stores a function that mutates a template. Functionally, a TemplatePatch scene will initialize a Default value of the patched Template if it does not already exist in the ResolvedScene, then apply the patch on top of the current Template in the ResolvedScene. Types that implement Template can generate a TemplatePatch like this:

#[derive(Template)]
struct MyTemplate {
    value: usize,
}

MyTemplate::patch_template(|my_template, context| {
    my_template.value = 10;
});

Likewise, types that implement GetTemplate can generate a patch for their template type like this:

#[derive(GetTemplate)]
struct Sprite {
    image: Handle<Image>,
}

Sprite::patch(|sprite_template| {
    // note that this is HandleTemplate<Image>
    sprite.image = "player.png".into();
})

We can now start composing scenes by writing functions that return impl Scene!

fn player() -> impl Scene {
    (
        Sprite::patch(|sprite| {
            sprite.image = "player.png".into();
        ),
        Transform::patch(|transform| {
            transform.translation.y = 4.0;
        }),
    )
}

The on() Observer / event handler Scene

on is a function that returns a scene that creates an Observer template:

fn player() -> impl Scene {
    (
        Sprite::patch(|sprite| {
            sprite.image = "player.png".into();
        ),
        on(|jump: On<Jump>| {
            info!("player jumped!");
        })
    )
}

The BSN Format

BSN is a new specification for defining Bevy Scenes. It is designed to be as Rust-ey as possible, while also eliminating unnecessary syntax and context. The goal is to make defining arbitrary scenes and UIs as easy, delightful, and legible as possible.

It is intended to be usable as both an asset format (ex: level.bsn files) and defined in code via a bsn! macro. These are notably compatible with each other. You can define a BSN asset file (ex: in a visual scene editor, such as the upcoming Bevy Editor), then inherit from that and use it in bsn! defined in code.

:"player.bsn"
Player
Sprite { image: "player.png" }
Health(10)
Transform {
	translation: Vec3 { y: 4.0 }
}
on(|jump: On<Jump>| {
	info!("player jumped!");
})
Children [
	(
		Hat
		Sprite { image: "cute_hat.png" }
		Transform { translation: Vec3 { y: 3.0 } } )
	),
	(:sword Transform { translation: Vec3 { x: 10. } } 
]

Note that this PR includes the bsn! macro, but it does not include the BSN asset format. It does include all of the in-memory / in-code support for the asset format. All that remains is defining a BSN asset loader, which will be done in a followup.

The bsn! Macro

bsn! is an optional ergonomic syntax for defining Scene expressions. It was built in such a way that Rust Analyzer autocomplete, go-to definition, doc hover, and semantic token syntax highlighting works as expected pretty much everywhere (but there are some gaps and idiosyncrasies at the moment, which I believe we can iron out).

It looks like this:

fn player() -> impl Scene {
    bsn! {
        Player
        Sprite { image: "player.png" }
        Health(10)
        Transform {
            translation: Vec3 { y: 4.0 }
        }
        on(|jump: On<Jump>| {
            info!("player jumped!");
        })
        Children [
            (
                Hat
                Sprite { image: "cute_hat.png" }
                Transform { translation: Vec3 { y: 3.0 } } )
            ),
            (:sword Transform { translation: Vec3 { x: 10. } } 
        ]
    }
}

fn sword() -> impl Scene {
    bsn! {
       Sword
       Sprite { image: "sword.png" } 
    }
}

fn blue_player() -> impl Scene {
    bsn! {
        :player
        Team::Blue
        Children [
            Sprite { image: "blue_shirt.png" } 
        ]
    }
}

I'll do a brief overview of each implemented bsn! feature now.

bsn!: Patch Syntax

When you see a normal "type expression", that resolves to a TemplatePatch as defined above.

bsn! {
    Player {
        image: "player.png"
    }
}

This resolve to the following:

<Player as GetTemplatePatch>::patch(|template| {
    template.image = "player.png".into();
})

This means you only need to define the fields you actually want to set!

Notice the implicit .into(). Wherever possible, bsn! provides implicit into() behavior, which allows developers to skip defining wrapper types, such as the HandleTemplate<Image> expected in the example above.

This also works for nested struct-style types:

bsn! {
    Transform {
        translation: Vec3 { x: 1.0 }
    }
}

Note that you can just define the type name if you don't care about setting specific field values / just want to add the component:

bsn! {
    Transform
}

To add multiple patches to the entity, just separate them with spaces or newlines:

bsn! {
    Player
    Transform
}

Enum patching is also supported:

#[derive(Component, GetTemplate)]
enum Emotion {
    Happy { amount: usize, quality: HappinessQuality },
    Sad(usize),
}

bsn! {
    Emotion::Happy { amount: 10. }
}

Notably, when you derive GetTemplate for an enum, you get default template values for every variant:

// We can skip fields for this variant because they have default values
bsn! { Emotion::Happy }

// We can also skip fields for this variant
bsn! { Emotion::Sad }

This means that unlike the Default trait, enums that derive GetTemplate are "fully patchable". If a patched variant matches the current template variant, it will just write fields on top. If it corresponds to a different variant, it initializes that variant with default values and applies the patch on top.

For practical reasons, enums only use this "fully patchable" approach when in "top-level scene entry patch position". Nested enums (aka fields on patches) require specifying every value. This is because the majority of types in the Rust and Bevy ecosystem will not derive GetTemplate and therefore will break if we try to create default variants values for them. I think this is the right constraint solve in terms of default behaviors, but we can discuss how to support both nested scenarios effectively.

Constructors also work (note that constructor args are not patched. you must specify every argument). A constructor patch will fully overwrite the current value of the Template.

bsn! {
    Transform::from_xyz(1., 2., 3.)
}

You can also use type-associated constants, which will also overwrite the current value of the template:

bsn! {
    Transform::IDENTITY
}

If you have a type that does not currently implement Template/GetTemplate, you have two options:

bsn! {
    // This will return a Template that produces the returned type.
    // `context` has World access!
	template(|context| {
	    Ok(TextFont {
	        font: context
	            .resource::<AssetServer>()
	            .load("fonts/FiraSans-Bold.ttf").into(),
	        ..default()
	    })
	})
	
	// This will return the value as a Template
	template_value(Foo::Bar)
}

bsn! Template patch syntax

Types that are expressed using the syntax we learned above are expected to implement GetTemplate. If you want to patch a Template directly by type name (ex: your Template is not paired with a GetTemplate type), you can do so using @ syntax:

struct MyTemplate {
    value: usize,
}

impl Template for MyTemplate {
    /* impl here */
}

bsn! {
    @MyTemplate {
        value: 10.
    }
}

In most cases, BSN encourages you to work with the final type names (ex: you type Sprite, not SpriteTemplate).

However in cases where you really want to work with the template type directly (such as custom / manually defined templates), "Template patch syntax" lets you do that!

bsn!: Inline function syntax

You can call functions that return Scene impls inline. The on() function that adds an Observer (described above) is a particularly common use case

bsn! {
    Player
    on(|jump: On<Jump>| {
        info!("Player jumped");
    })
}

bsn!: Relationship Syntax

bsn! provides native support for spawning related entities, in the format RelationshipTarget [ SCENE_0, ..., SCENE_X ]:

bsn! {
    Node { width: Px(10.) } 
    Children [
        Node { width: Px(4.0) },
        (Node { width: Px(4.0) } BackgroundColor(srgb(1.0, 0.0, 0.0)),
    ]
}

Note that related entity scenes are comma separated. Currently they can either be flat or use () to group them:

bsn! {
    Children [
        // Child 1
        Node BorderRadius::MAX,
        // Child 2
        (Node BorderRadius::MAX),
    ]
}

It is generally considered best practice to wrap related entities with more than one entry in () to improve legibility.

bsn!: Expression Syntax

bsn! supports expressions in a number of locations using {}:

let x: u32 = 1;
let world = "world";
bsn! {
    // Field position expressions
    Health({ x + 2 })
    Message {
        text: {format!("hello {world}")}
    }
}

Expressions in field position have implicit into().

Expressions are also supported in "scene entry" position, enabling nesting bsn! inside bsn!:

let position = bsn! {
    Transform { translation: Vec3 { x: 10. } }
};

bsn! {
    Player
    {position}
}

bsn!: Inline variables

You can specify variables inline:

let black = Color::BLACK;
bsn! {
    BackgroundColor(black)
}

This also works in "scene entry" position:

let position = bsn! {
    Transform { translation: Vec3 { x: 10. } }
};

bsn! {
    Player
    position
}

Inheritance

bsn! uses : to designate "inheritance". Unlike defining scenes inline (as mentioned above), this will pre-resolve the inherited scene, making your current scene cheaper to spawn. This is great when you inherit from large scene (ex: an asset defined by a visual editor). Scenes can only inherit from one scene at a time, and it must be defined first.

You can inherit from scene assets like this:

fn red_button() -> impl Scene {
    bsn! {
        :"button.bsn"
        BackgroundColor(RED)
    }
}

Note that while there is currently no implemented .bsn asset format, you can still test this using AssetServer::load_with_path.

You can also inherit from functions that return a Scene:

fn button() -> impl Scene {
    bsn! {
        Button
        Children [
            Text("Button")
        ]
    }
}

fn red_button() -> impl Scene {
    bsn! {
        :button
        BackgroundColor(RED)
    }
}

Note that because inheritance is cached / pre-resolved, function inheritance does not support function parameters. You can still use parameterized scene functions by defining them directly in the scene (rather than using inheritance):

fn button(text: &str) -> impl Scene {
    bsn! {
        Button
        Children [
            Text(text)
        ]
    }
}

fn red_button() -> impl Scene {
    bsn! {
        button("Click Me")
        BackgroundColor(RED)
    }
}

Related entities can also inherit:

bsn! {
    Node
    Children [
        (:button BackgroundColor(RED)),
        (:button BackgroundColor(BLUE)),
    ]
}

Inheritance concatenates related entities:

fn a() -> impl Scene {
    bsn! {
        Children [
            Name("1"),
            Name("2"),
        ]
    }
}

fn b() -> impl Scene {
    /// this results in Children [ Name("1"), Name("2"), Name("3") ]
    bsn! {
        :a
        Children [
            Name("3"),
        ]
    }
}

bsn_list! / SceneList

Relationship expression syntax {} expects a SceneList. Many things, such as Vec<S: Scene> implement SceneList allowing for some cool patterns:

fn inventory() -> impl Scene {
    let items = (0..10usize)
        .map(|i| bsn! {Item { size: {i} }})
        .collect::<Vec<_>>();
    bsn! {
        Inventory [
            {items}
        ]
    } 
}

The bsn_list! macro allows defining a list of BSN entries (using the same syntax as relationships). This returns a type that implements SceneList, making it useable in relationship expressions!

fn container() -> impl Scene {
    let children = bsn_list! [
        Name("Child1"),
        Name("Child2"),
        (Name("Child3") FavoriteChild),
    ]
    bsn! {
        Container [
            {children}
        ]
    } 
}

This, when combined with inheritance, means you can build abstractions like this:

fn list_widget(children: impl SceneList) -> impl Scene {
    bsn! {
        Node {
            width: Val::Px(1.0)
        }
        Children [
            Text("My List:")
            {children}
        ]
    }
}

fn ui() -> impl Scene {
    bsn! {
        Node
        Children [
            list_widget({bsn_list! [
                Node { width: Px(4.) },
                Node { width: Px(5.) },
            ]})
        ]
    }
}

bsn!: Name Syntax

You can quickly define Name components using #Name shorthand.

bsn! {
    #Root
    Node
    Children [
        (#Child1, Node),
        (#Child2, Node),
    ]
}

#MyName produces the Name("MyName") component output.

Within a given bsn! or bsn_list! scope, #Name can also be used in value position as an Entity Template:

#[derive(Component, GetTemplate)]
struct UiRoot(Entity);

#[derive(Component, GetTemplate)]
struct CurrentButton(Entity);

bsn! {
	#Root
	CurrentButton(#MyButton)
	Children [
		(
		  #MyButton,
		  UiRoot(#Root)
		)
	]
}

These behave a bit like variable names. In the context of inheritance and embedded scenes, #Name is only valid within the current "scene scope":

fn button() -> impl Scene {
	bsn! {
		#Button
		Node
		Children [
			ButtonRef(#Button)
		]
	}
}

fn red_button() -> impl Scene {
	bsn! {
		:button
		// #Button is not valid here, but #MyButton
		// will refer to the same final entity as #Button
		#MyButton
		Children [
			AnotherReference(#MyButton)
		]
	}
}

In the example above, because #MyButton is defined "last" / is the most "specific" Name, the spawned entity will have Name("MyButton")

Name references are allowed to conflict across inheritance scopes and they will not interfere with each other.

#Name can also be used in the context of bsn_list!, which enables defining graph structures:

bsn_list! [
	(#Node1, Sibling(#Node2)),
	(#Node2, Sibling(#Node1)),
]

Name Restructure

The core name component has also been restructured to play nicer with bsn!. The impl on main requires Name::new("MyName"). By making the name string field public and internalizing the prehash logic on that field, and utilizing implicit .into(), we can now define names like this:

bsn! {
    Name("Root")
    Children [
        Name("Child1"),
        Name("Child2"),
    ]
}

BSN Spawning

You can spawn scenes using World::spawn_scene and Commands::spawn_scene:

world.spawn_scene(bsn! {
    Node
    Children [
        (Node BackgroundColor(RED))
    ]
})?;

commands.spawn_scene(widget());

The spawn_scene operation happens immediately, and therefore assumes that all of the Scene's dependencies have been loaded (or alternatively, that there are no dependencies). If the scene has a dependency that hasn't been loaded yet, World::spawn_scene will return an error (or log an error in the context of Commands::spawn_scene).

If your scene has dependencies, you can use World::queue_spawn_scene and Commands::queue_spawn_scene. This will spawn the entity as soon as all of the Scene's dependencies have been loaded.

// This will spawn the entity once the "player.bsn" asset is loaded
world.queue_spawn_scene(bsn! {
  :"player.bsn"
  Transform { position: Vec3 { x: 10. } }
});

There are also spawn_scene_list variants for everything above:

world.spawn_scene_list(bsn_list! [
	button("Ok"),
	button("Cancel"),
])

EntityWorldMut and EntityCommands also have some new functionality:

entity.queue_spawn_related_scene::<Children>(bsn_list! [
	(:"player.bsn", #Player1),
	(:"player.bsn", #Player2),
]);
entity.apply_scene(bsn! {
	Transform { position: Vec3 { x: 10. } }
})?;

For scene assets, you can also just add the ScenePatchInstance(handle) component, just like the old Bevy scene system.

VariantDefaults derive

GetTemplate automatically generates default values for enum Template variants. But for types that don't use GetTemplate, I've also implemented a VariantDefaults derive that also generates these methods.

What's Next?

Must happen before 0.19

  • Sort out bevy_scene vs bevy_scene2: The current plan is to rename bevy_scene to bevy_ecs_serialization, and remove "scene" terminology from it. That then frees up bevy_scene2 to be renamed to bevy_scene. The current bevy_scene will need to exist for awhile in parallel to BSN, as BSN is not yet ready for "full world serialization" scenarios.
  • Resolve the Default Handle situation: Currently, to provide Template support for Handle, it implements GetTemplate. This of course conflicts with impl Default for Handle. This is pretty disruptive to non-BSN users (which is currently everyone). We'll want to sort out a middleground solution in the short term that ideally allows us to keep impl Default for Handle during the transition.
  • Nested bsn! Scene tuples to surpass tuple impl limits

Ideally before 0.19

We likely won't land all of these. The plan is to (ideally) land this PR before Bevy 0.19 RC1, then maybe land a couple more of these before

  • Feathers BSN Port: Largely already done. Just need to reconcile with current state of main. This will help BSN land well, so landing it alongside BSN is a high priority.
  • ResolvedScene-as-dynamic-bundle: ResolvedScene should insert all of the components at once as a single bundle, rather than one-by-one, which is really bad from an archetype move perspective. Without this, using world.spawn_scene(scene) as a world.spawn(bundle) replacement will result in a pretty significant performance reduction.
  • #Name references in more places: The UI eventing scenario really wants #Name to be usable in closures. This would functionally be expressed as a template that returns a closure that accesses a specific entity. This unlocks a lot of value for UI devs, so ideally it lands alongside BSN.
  • Top-down vs bottom-up spawn order: Currently BSN follows the normal bevy top-down spawn order. I think we should heavily consider spawning bottom-up, in the interest of making scene contents available to "higher level" components in their lifecycle events (ex: a Player component accessing nested entities like "equipment" when inserted). If we decide to keep things as they are, we probably want to introduce additional "scene ready" entity events that trigger "bottom up".
  • Inline field value expressions: Support cases such as `px(10).all()
  • Add EntityPath to EntityTemplate: Support resolving entity paths (ex: "Root/Child1/GrandChild1"). This is relatively low hanging fruit, especially if we switch to bottom-up spawning order.
  • Function Inheritance Caching: Currently only scene asset inheritance is pre-computed / cached. For consistency / predictability / optimizations, function inheritance (ex :button) should also be cached.
  • derive(GetTemplate) generics ergonomics: Currently this requires casting spells: T: GetTemplate<Template: Default + Template<Output = T>>

Near Future

  • BSN Asset Format: Add a .bsn parser / AssetLoader that can produce the current ScenePatch assets.
  • Struct-style inheritance: It would be nice to be able to do something like :Button { prop } instead of :button(prop). I'd really like us to explore this being component-tied (ex: associate a scene with a Button component).
  • Descendant Patching: It should be possible to "reach in" to an inherited scene and patch one of its descendants / children.
  • Optimize Related Entity Spawning: This currently inserts the relationship component first, then spawns the related scene. This results in an unnecessary archetype move.
  • Observers as relationships
  • Scene-owned-entities: Currently when spawning a Scene, every entity defined in the scene is instantiated. Some scenarios would benefit from Scene instances sharing some unique entity. For example: defining assets inside of scenes (this would pair nicely with Assets as Entities) , sharing Observer entities, etc.
  • The touch_type::<Nested>() approach could be replaced with let x: &mut Nested for actual type safety (and probably better autocomplete)
  • Fix Rust Analyzer autocomplete bug that fails to resolve functions and enums for <Transform as GetTemplate>::Template::from_transform()

Longer Term

  • bsn! hot patching via subsecond: Proof of concept here
  • Reactivity: This has been proven out here
  • BSN Sets: See the old design doc for the design space I'm talking about here
    • This would also allow expressing "flattened" forms of BSN, which makes diffs easier to read in some case
  • World to BSN: If we can support this, BSN can be used for things like saving Worlds to disk. This might also be useful for building scene editors.

@cart cart added this to the 0.19 milestone Mar 18, 2026
@cart cart added A-ECS Entities, components, systems, and events A-Scenes Serialized ECS data stored on the disk X-Needs-SME This type of work requires an SME to approve it. labels Mar 18, 2026
@github-project-automation github-project-automation bot moved this to Needs SME Triage in ECS Mar 18, 2026
@cart cart force-pushed the next-generation-scenes branch 2 times, most recently from a660bc5 to a664977 Compare March 18, 2026 23:09

/// [`GetTemplate`] 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.
pub trait GetTemplate: Sized {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a bit unnatural because the trait has an associated type representing a template. Yes, it can be used for getting a template, but there are no getters.

If it was discussed in some RFC and all the parties agreed on it, feel free to discard my comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think HasTemplate is also a reasonable name. However it doesn't have a has() function either. And the purpose of the trait is not to know whether or not it has a Template, but rather to get the template associated with the type. Of the two names, I think GetTemplate is better. I'm definitely open to other names though!

Copy link
Member

@YohDeadfall YohDeadfall Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be Templatable then and has no verbs, but telling that the implementing type can be templated using the specified associated type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer Templatable as well, but I would also be fine with FromTemplate, to mirror FromWorld.

Copy link
Member Author

@cart cart Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My issue with Templatable it is that every type T is theoretically "template-able" / could have zero to many types that implement Template where Template<Output = T>. GetTemplate is about a type T having a canonical template.

Templated feels slightly better semantically, as it doesn't express whether something is "template-able", but rather it says "T has a template" (which is perhaps close enough to the "has a canonical template" idea). The "problem" I see here is the similarity between Template and Templated (just a d character differentiating them).

FromTemplate is pretty reasonable. Slight "weirdness factor", given that it isn't implementing From logic in the trait, but rather deferring to the Template for that logic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option is WithTemplate. It's similar to HasTemplate, but avoids implying a has() method that doesn't exist.

@cart cart force-pushed the next-generation-scenes branch from a664977 to cfd3a96 Compare March 18, 2026 23:20
use tracing::error;

/// Adds scene spawning functionality to [`World`].
pub trait WorldScenes {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this WorldScenesExt? I personally prefer making extension traits very clear to indicate that you're not meant to implement this or reference it.

Same thing for all the following extension traits!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I've always kind of disliked the Ext naming, as this is still a trait that behaves just like any other. In a way all traits are Ext. Ext always felt like a failure to describe the scope of the trait.

That being said, I don't have strong opinions here and Ext is used in a number of places elsewhere in Bevy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're not meant to implement this or reference it

I don't think either of these are strict requirements. I think a manual import of WorldScenes is totally valid, as is someone choosing to implement it and use it to share logic between things that perform the same operations.

Co-authored-by: andriyDev <andriydzikh@gmail.com>
@alice-i-cecile alice-i-cecile added S-Needs-Review Needs reviewer attention (from anyone!) to move forward M-Release-Note Work that should be called out in the blog due to impact labels Mar 19, 2026
@github-actions
Copy link
Contributor

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

/// ]
/// });
/// ```
fn queue_spawn_scene<S: Scene>(&mut self, scene: S) -> EntityWorldMut<'_>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of things:

  1. I think this should be spawn_scene_queued for LSP discoverability
  2. I think there's a strong argument for the queued behaviour being the default, and renaming the version that assumes dependencies are already loaded spawn_scene_<suffix that communicates that we're not loading new dependencies>

Copy link
Member Author

@cart cart Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Yeah I think thats reasonable
  2. I think in the context of "bsn! everywhere", the vast majority of spawns could be done "immediately". That was the rationale here. Perhaps the right "best of both worlds" path here is to change the behavior of spawn_scene_queued to eagerly resolve and spawn when possible, then give it the preferred spawn_scene name / use it everywhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it makes sense as a rationale, I'm just imagining the world where a newcomer doesn't understand why their scene's dependencies aren't spawning when they use the shortest (and therefore "most canonical") scene spawning API after spending most of their time, enthusiasm, and attention in design tooling.

/// desired template output type. This is used by Bevy's scene system.
pub trait GetTemplate: Sized {
/// The [`Template`] for this type.
type Template: Template;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the GetTemplate / Template traits fit with FromWorld?

My initial read of them as "ways to initialiaze objects that require World data" puts them in exactly the same niche.

Do you feel that we'll want both in the long-term? Can we better explain subtle distinctions using docs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar niches, but still quite different / they can and should exist next to each other:

  1. FromWorld: creates T from a world reference
  2. GetTemplate: creates T from an entity spawn context, which notably includes an EntityWorldMut reference and "scoped entity reference" information from the current scene spawn.

FromWorld fills niches that GetTemplate does not (initializing any type that is not an entity). FromWorld is also still the better pick for Resources. We could in theory try to replace FromWorld in that context, but it would require reworking some of the resource init code to make it efficient. And it would be a breaking change, which I think merits consideration + planning.

}
}

/// [`GetTemplate`] is implemented for types that can be produced by a specific, canonical [`Template`]. This creates a way to correlate to the [`Template`] using the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explanation of this in the PR description is much better; we should duplicate the subtle information about usage / intent here.

Also this needs to link out to docs about the derive macro, which should mention that it generates types for you.

use downcast_rs::{impl_downcast, Downcast};
use variadics_please::all_tuples;

/// A [`Template`] is something that, given a spawn context (target [`Entity`], [`World`](crate::world::World), etc), can produce a [`Template::Output`].
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, lots of useful context from the PR description that should be captured in the docs here.

/// [`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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Scenes are free to modify these lists, but in most cases they should probably just be pushing to the back of them." from the PR description.

) -> Result<(), ResolveSceneError>;

/// [`Scene`] can have [`Asset`] dependencies, which _must_ be loaded before calling [`Scene::resolve`] or it might return a [`ResolveSceneError`]!
///
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The scene system will ensure Scene::resolve is not called until all of the dependencies have loaded"

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.
///
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Scene is always one top level / root entity. For "lists of scenes" (such as a list of related entities), we have the SceneList trait, which can be used in any place where zero to many scenes are expected. These are separate traits for logical reasons: world.spawn() is a "single entity" action, scene inheritance only makes sense when both scenes are single roots, etc."


all_tuples!(scene_impl, 0, 12, P);

/// A [`Scene`] that patches a [`Template`] of type `T` with a given function `F`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Functionally, a TemplatePatch scene will initialize a Default value of the patched Template if it does not already exist in the ResolvedScene, then apply the patch on top of the current Template in the ResolvedScene. Types that implement Template can generate a TemplatePatch like this:"

all_tuples!(scene_impl, 0, 12, P);

/// A [`Scene`] that patches a [`Template`] of type `T` with a given function `F`.
pub struct TemplatePatch<F: Fn(&mut T, &mut ResolveContext), T>(pub F, pub PhantomData<T>);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docs need more motivation, explaining when / why you might want to use this type. A single tangible example would go a long way.

))
.map_err(|e| ApplySceneError::RelatedSceneError {
relationship_type_name: related.relationship_name,
index,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right index? index above is defined by the outer loop, but used by the inner loop.

More descriptive names would help clarify what this code is doing.

pub fn apply(&self, context: &mut TemplateContext) -> Result<(), ApplySceneError> {
if let Some(inherited) = &self.inherited {
let scene_patches = context.resource::<Assets<ScenePatch>>();
if let Some(patch) = scene_patches.get(inherited)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This silently fails. scene_patches::get should probably be returning an error that we can bubble up.

}
}
PathType::Const => {
todo!("A floating type-unknown const should be assumed to be a const scene right?")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should clean this up before merging probably.

BsnValue::Name(input.parse::<Ident>()?)
} else {
return Err(input.error(
"BsnValue parse for this input is not supported yet, nor is proper error handling :)"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably get a TODO comment at least so we can find it.


/// This is used to register information about the template, such as dependencies that should be loaded before it is instantiated.
#[inline]
fn register_data(&self, _data: &mut TemplateData) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code and the TemplateData type are never called. We should at least add a test in this PR.


/// This is used by the [`GetTemplate`] 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 Wrapper<T> = T;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name is painfully generic.

}

/// A [`Template`] reference to an [`Entity`].
pub enum EntityReference {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What other variants do we want to support in the future here?


fn build_template(&self, context: &mut TemplateContext) -> Result<Self::Output> {
Ok(match self {
// unwrap is ok as this is "internals". when implemented correctly this will never panic
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem totally clear what unwrap this is referring to.

related: TypeIdMap<RelatedResolvedScenes>,
/// The inherited [`ScenePatch`] to apply _first_ before applying this [`ResolvedScene`].
inherited: Option<Handle<ScenePatch>>,
/// A [`TypeId`] to `templates` index mapping. If a [`Template`] is intended to be shared / patched across scenes, it should have
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sentence seems incomplete. It should have what?

Comment on lines +3 to +13
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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought bsn was going to be a declarative macro, this is disappointing. proc macros are pretty brittle, they often break with proc macro not expanded: proc macro crate is missing dylib errors. even println!'s autocomplete and formatting breaks pretty often for me, and thats a native rust feature. do we expect to do better in userland?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my knowledge, declarative macros are not flexible enough to support something like bsn!. I'd be stoked if someone found a way, but the parsing and codegen logic go well beyond what I believe is capable in macro_rules!.

bsn! was built with autocomplete / ide integration in mind. It isn't 100% bulletproof atm, but apart from a few corner cases I'm still working on, I think it is pretty close to ideal. I recommend you give it a try so we can discuss the reality of the situation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the topic of formatting, rustfmt would not be able to format bsn! (and would refuse to try in the context of the {} in bsn! {}). The plan here is to write our own BSN formatting tool, which can be used for both rust code and .bsn assets.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintaining a formatter sounds like a pretty big maintenance commitment, would that be part of the bevy cli?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're defining a new syntax, I personally think we're obligated to provide a degree of "tooling" for it (grammar files for syntax highlighting, formatting tooling, etc).

Yes it would be part of the Bevy CLI. It is a maintenance commitment, but I believe it would be relatively scoped (largely build it and forget it).

Copy link
Member Author

@cart cart Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we already get part way there for formatting just by writing an in-memory-BSN -> .bsn serializer (which we will need to do anyway).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events A-Scenes Serialized ECS data stored on the disk M-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Needs-SME This type of work requires an SME to approve it.

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.