Skip to content

Next Generation Bevy Scenes #20158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft

Conversation

cart
Copy link
Member

@cart cart commented Jul 16, 2025

Welcome to the draft PR for BSN (pronounced "B-Scene", short for Bevy SceNe), my proposal for Bevy's next generation Scene / UI system. This an evolution of my first and second written proposals. Much has changed since then, but much is also the same. I'm excited to see what everyone thinks!

First some expectation setting: this is not yet a shippable product. It is not time to start porting your games to it. This is unlikely to land in the upcoming Bevy 0.17, but very likely to land in some form in Bevy 0.18. It is starting to become usable (for example I have ported the work-in-progress Bevy Feathers widgets to it), but there are still gaps left to fill and likely many rounds of feedback and tweaking as others form opinions on what works and what doesn't.

What should the "review approach" be?

This PR is not intended to be merged in its current form. Now is not the time to polish things like docs / tests / etc ... save those comments for the next phase. This is a "public experimentation phase" where we can collectively evaluate my proposed approach and flexibly pivot / iterate / tweak as necessary.

If functionality is missing, it is worth discussing whether or not to implement it at this stage, as the goal is to reach an agreed-upon MVP state as soon as possible. Likewise for bugs: if something critical is broken that should be called out and fixed.

Please use threaded discussions by leaving comments on locations in code (even if you aren't talking about that line of code). I'm going to aggressively clean up top level discussions to avoid burying things ... you have been warned.

If you would like to propose changes or add features yourself, feel free to create a PR to this branch in my repo and we can conduct a discussion and review there. This will be a long-living branch that I regularly sync with main.

When we're ready to start polishing and merging into main, I'll create new PRs (with smaller scopes) so we can iteratively review / polish / merge in smaller chunks.

Note that next week I'll be on vacation. Feel free to continue discussing here while I'm gone and I'll respond when I get back.

What is currently included?

From a high level, this draft PR includes Templates, core Scene traits and types, scene inheritance, and the bsn! macro. This is enough to define scenes in code, and also provides the framework for scene formats to "slot in". This also includes a port of the Bevy Feathers widget framework to bsn!, so look there for "real world" usage examples.

What is not currently included?

This does not include the BSN asset format / loader (ex: asset_server.load("level.bsn")). I've implemented this in my previous experiments, but it hasn't yet been updated the latest approach. This will be done in a followup: not including it allows us to focus on the other bits first and perhaps get a usable subset of non-asset functionality merged first.

This also does not include any form of "reactivity". The plan here is to allow for an experimentation phase that tries various approaches on top of this core framework. If the framework cannot accommodate a given approach in its current form (ex: coarse diffing), we can discuss adding or changing features to accommodate those experiments.

See the MVP Stretch Goals / Next Steps section for a more complete list.

Overview

This is a reasonably comprehensive conceptual overview / feature list.

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(&mut self, entity: &mut EntityWorldMut) -> 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.
  • Mutable / stateful: Building a Template can involve storing/changing state on that Template. This enables caching / state-sharing behaviors.

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(&mut self, entity: &mut EntityWorldMut) -> Result<Handle<T>> {
        Ok(entity.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 EntityPath to the Entity it points to (this has been shimmed in, but I haven't yet built actual EntityPath resolution).

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(&mut self, _entity: &mut EntityWorldMut) -> 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.

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 patch(&self, assets: &AssetServer, patches: &Assets<ScenePatch>, scene: &mut ResolvedScene);
    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::patch applies the current 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. Scene patches 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::patch. The scene system will ensure Scene::patch 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. SceneList is implemented for tuples of SceneList, and the EntityScene<S: Scene>(S) wrapper type. This wrapper type is necessary for the same reasons the Spawn<B: Bundle>(B) wrapper is necessary when using the children![] macro in Bundles: not using the wrapper would cause conflicting impls.

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| {
    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!");
        })
    )
}

on is built in such a way that when we add support for adding additional observer target entities at runtime, a single observer can be shared across all instances of the scene. on does not "patch" existing templates of the same type, meaning multiple observers of the same event are possible.

bsn! Macro

bsn! is an optional ergonomic syntax for defining Scene expressions. It is built 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 was built in such a way that Rust Analyzer autocomplete, go-to definition, and doc hover works as expected pretty much everywhere.

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!");
        })
        [
            (
                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
        [
            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:

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
}

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.
    }
}

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),
    ]
}

bsn! also supports [] shorthand for children relationships:

bsn! {
    Node { width: Px(10.) }  [
        Node { width: Px(4.0) },
        Node { width: Px(4.0) },
    ]
}

It is generally considered best practice to wrap related entities with more than one entry in () to improve legibility. One notable exception is when you have one Template patch and then children:

bsn! {
    Node { width: Px(10.) }  [
        Node { width: Px(4.0) } [
            // this wraps with `()` because there are too many entries
            (
                Node { width: Px(4.0) }
                BackgroundColor(RED)
                [ Node ]
            )
        ]
    ]
}

Ultimately we should build auto-format tooling to enforce such conventions.

bsn!: Expression Syntax

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

// Field position
let x: u32 = 1;
let world = "world";
bsn! {
    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".

You can inherit from other functions that return a Scene:

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

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

You can pass arguments to inherited functions:

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

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

You can inherit from scene assets:

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 by registering in-memory assets. See the bsn.rs example.

Related entities can also inherit:

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

Multiple inheritance is also supported (and applied in the order it is defined):

bsn! {
    :button
    :rounded
    BackgroundColor(RED)
}

Inheritance concatenates related entities:

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

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

As a matter of convention, inheritance should be specified first. An impl Scene is resolved in order (including inheritance), so placing them at the top ensures that they behave like a "base class". Putting inheritance at the top is also common practice in language design, as it is high priority information. We should consider enforcing this explicitly (error out) or implicitly (always push inheritance to the top internally).

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)
        }
        [
            Text("My List:")
            {children}
        ]
    }
}

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

bsn!: Name shorthand syntax

You can quickly define Name components using #Name shorthand. This might be extended to be usable in EntityTemplate position, allowing cheap shared entity references throughout the scene.

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

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") [
        Name("Child1"),
        Name("Child2"),
    ]
}

BSN Spawning

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

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

commands.spawn_scene(widget());

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

template Scene function

If you would like to define custom ad-hoc non-patched Template logic without defining a new type, you can use the template() function, which will return a Scene that registers your function as a Template. This is especially useful for types that require context to initialize, but do not yet use GetTemplate / Template:

bsn! {
    Text("hello world")
    template(|context| {
        Ok(TextFont {
            font: context
                .resource::<AssetServer>()
                .load("fonts/FiraSans-Bold.ttf"),
            font_size: 33.0,
            ..default()
        })
    })
}

template_value Scene function

To pass in a Template value directly, you can use template_value:

bsn! {
    Node
    template_value(MyTemplate)
}

There is a good chance this will get its own syntax, as it is used commonly (see the bevy_feathers widgets).

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.

Bevy Feathers Port

This was pretty straightforward. All widget functions now use bsn! and return impl Scene instead of returning impl Bundle. Callback now implements GetTemplate<Template = CallbackTemplate>. I've added a callback function that returns a CallbackTemplate for the given system function. I believe this wrapper function is necessary due to how IntoSystem works.

I highly recommend checking out the widget implementations in bevy_feathers and the widget usages in the feathers.rs example for "real world" bsn! usage examples.

MVP TODO

This is roughly in priority order:

  • Resolve MarkerComponent [CHILDREN] vs Observers [OBSERVERS] ambiguity
  • Efficient "layered" inheritance. Currently we're "baking" every spawned instance. Implementing this is important for making this performant enough in practice for large complicated scenes and/or high-frequency spawning of inherited scenes.
  • Explore struct-style inheritance (ex: :Button { label: "hello" })
  • Expand #Name syntax to be usable in EntityTemplate position for cheap entity references throughout the scene. This will likely involve replacing Template::build(entity: EntityWorldMut) with a wrapper TemplateContext, so we can cache these entity references.
  • Optimize inserted ResolvedScenes by treating them as dynamic bundles
  • Preallocate space in relationship collections prior to insert (this might tie into the previous point)
  • Experiment with reactivity impls / ensure bsn! can accommodate them.
  • Investigate defining inline scene inputs inside bsn!. This may tie in to reactivity
  • Additional Template lifecycle methods
    • Template::remove()?
    • Template::update()?
  • Actually implement EntityPath resolution (currently just a stub)
  • Supporting arbitrary generic parameters requires casting spells: T: GetTemplate<Template: Default + Template<Output = T>>. This is similar to the Reflect problem, but uglier. The derive should be adjusted to remove this requirement.
  • Investigate supporting inherited scenes "intercepting" children from the inheritor. This may also tie into "inline scene inputs".
  • Add a standalone Template derive (for template types that don't want GetTemplate)
  • touch_type::<Nested>() approach could be replaced with let x: &mut Nested for actual type safety
  • investigate caching inherited function Scenes, rather than creating new instances each time
  • support normal rust spread syntax (ex: ..Default::default() and the upcoming ..)
  • Support Thing<X> {} in addition to Thing::<X> {}. We can defeat the turbofish!
  • rustfmt isn't working in the codegen.rs file
  • Template convenience methods (ex: world.spawn_template(), entity.insert_template(), etc)
  • Tests
  • More Docs
  • Explore dynamic field-based lists for patches, which would BSN diffing better / more granular
    • Would be slower, so might need to be opt-in (or at least, we generate both efficient closure patches and dynamic field lists)
    • Would probably need to be Godot-esque Variant type to avoid too many allocations.
    • Same types could potentially be used for "inline scene inputs" and the in-memory BSN Asset representation
  • Fix Bugs:
    • Autocomplete for patched fields includes functions
      • Could try destructuring every field to constrain the autocomplete space
    • Scene asset path-loading-hack approach used in bsn.rs example sometimes hangs (race condition!)

MVP Stretch Goals / Next Steps

  • bsn! hot patching via subsecond
  • BSN Asset Format
  • Reactivity
  • 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
  • BSN Style System (see the old design doc)
    • Inheritance is a nice stop-gap styling solution, but a dedicated style system would significanty improve UX.
  • Scene lifecycle events (ex: On<SceneReady>, which is fired when an entity's scene templates have been applied and all of its children have fired On<SceneReady>)
  • Specific / named child patching: the ability to "reach in" to an inherited child and change it. This is most important in the context of BSN assets overriding properties in inherited scenes (Godot supports this and I personally couldn't live without it when visually editing scenes).
  • Fix Rust Analyzer autocomplete bug that fails to resolve functions and enums for <Transform as GetTemplate>::Template::from_transform()
  • Investigate adding related entities via a passed in entity reference (rather than spawning a new entity)
  • Tuple patching currently requires specifying the fields up to the field you want to patch
    • Can we use "rust tuple struct index init syntax" for this?

Discussion

I've created a number of threads with discussion topics. Respond to those threads and/or create your own. Please do not have top-level unthreaded conversations or comments. Create threads by leaving a review comment somewhere in the code (even if you can't find a good line of code to leave your comment). Before creating a new thread for a topic, check to see if one already exists!

@cart cart added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events A-UI Graphical user interfaces, styles, layouts, and widgets C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Scenes Serialized ECS data stored on the disk X-Controversial There is active debate or serious implications around merging this PR labels Jul 16, 2025
@cart cart marked this pull request as draft July 16, 2025 04:30
Copy link
Member Author

@cart cart left a comment

Choose a reason for hiding this comment

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

Some initial discussions / FAQs

#[reflect(Component, Default, Clone, PartialEq)]
pub struct Mesh2dWireframe(pub Handle<Wireframe2dMaterial>);

impl Default for Mesh2dWireframe {
Copy link
Member Author

Choose a reason for hiding this comment

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

Whats the deal with all of these manual Default impls?

Handle no longer implements default. This is because it now implements GetTemplate. Implementing Default would make it match the blanket impl of GetTemplate for T: Default + Clone, resulting in conflicting GetTemplate impls. This is in line with our goal of making Handle "always strong" (ex: removing the Handle::Uuid variant). That means that anything using Handle will need to derive GetTemplate instead of Default.

Rather than port everything over now, I've added a Handle::default() method, and anything currently relying on default handles now has a manual Default impl that calls Handle::default() instead of Default::default().

Copy link
Member

Choose a reason for hiding this comment

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

I presume that Handle::default will be fully removed once the porting is complete? The diff may be easier to read and grep for it we call it something else: either Handle::null or Handle::temporary_default would work well.


fn button(label: &'static str) -> impl Scene {
bsn! {
Button
Copy link
Member Author

@cart cart Jul 16, 2025

Choose a reason for hiding this comment

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

Why does bsn! not separate Scene patch entries with commas? That doesn't feel very Rust-ey

  1. Flat entities (no wrapper tuple) are allowed. In Rust, x, y, z is not a valid expression
  2. Unlike previous proposals, relationships are now expressed as peers of the components (old proposal: (COMPONENTS) [CHILDREN], new proposal (COMPONENTS [CHILDREN])). I think in all cases, they are more legible without commas, especially in the very simple / very common case of SomeComponent [ CHILDREN ]. The comma version of this looks weird, especially without the tuple wrapper: SomeComponent, [].
  3. Removing the commas helps visually group entities when they would otherwise be lost in a sea of punctuation. I think that in practice this is extremely noticeable (try adding the commas to the various bsn! examples and establish your own opinions).

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Removing the commas helps visually group entities when they would otherwise be lost in a sea of punctuation.

I'm not convinced its more readable - I prefer trailing commas, e.g.

BorderColor::from(Color::BLACK),
BorderRadius::MAX,
BackgroundColor(Color::srgb(0.15, 0.15, 0.15)),

As a bonus, it pairs better with the straight rust code - this makes it easier to interchange between the two.

There's another example in examples/ui/feathers.rs in fn demo_root in the first button. Below is with the comma's, which I think is easier to read tbh

                    (
                        :button(ButtonProps {
                            on_click: callback(|_: In<Activate>| {
                                info!("Normal button clicked!");
                            }),
                            ..default()
                        }),
                        [(Text::new("Normal"), ThemedText)],
                    ),

Copy link
Contributor

Choose a reason for hiding this comment

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

I think for children we should be more explicit than just [ ] , I think @[ ] could be good (would pair nicely with ${ } for expressions as mentioned in a different thread). For a beginner, it's easier to search Bevy docs for @[ than it would be for [ . It also means we could use similar syntax in the future for other relationships / features.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not trying to argue for commas (though I kinda like them), but a simple mitigation for 1 is to simply use parentheses for the macro: bsn!(A, B, C) and it looks like a valid tuple again.

As far as 2 (probably more controversial), I probably won't use the children shorthand. I like that Children isn't special-cased in the ECS, it's "just another component", and the shorthand syntax kinda obfuscates that. I'd be much more likely to use Children [ ... ] for consistency in my own scenes.

I think 3 is a lot more opinion-based, though, and I don't have anything to add.

I realize commas aren't the most important detail and I'm not trying to waste time with a big argument. Just my two cents.

Copy link
Contributor

@dmyyy dmyyy Jul 16, 2025

Choose a reason for hiding this comment

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

I don't like the choice to not have commas between components - I would prefer a more rusty syntax with commas delimiting all components. I would also prefer the distinction between what is a component and what is an entity to be more explicit - I think it would be nice if all entities can be marked with # - I think it would make it easy to see what is an entity and what is a component at a glance. This would make #{name}(...) required for each entity. It has the added advantage of allowing users to build a correct mental model about what relationships look like (a collection of entities).

edit: not sure if intentional but the # prefix == entity association is also a nice reminder that an entity is just an 8 byte number - whereas a component can be something larger.

    bsn! {
        #Root(
            Player,
            Sprite { image: "player.png" },
            Health(10),
            Transform {
                translation: Vec3 { y: 4.0 }
            },
            on(|jump: On<Jump>| {
                info!("player jumped!");
            }),
            [
                #NamedEntity(
                    Hat,
                    Sprite { image: "cute_hat.png" },
                    Transform { translation: Vec3 { y: 3.0 } } ),
                ),
                // still an entity - but without a name
                #(:sword, Transform { translation: Vec3 { x: 10. } } ),
            ]
        )
    }

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`].
pub trait Template {
Copy link
Member Author

Choose a reason for hiding this comment

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

What happened to Construct?

Template fills a similar role to my previously proposed Construct trait. The core difference is that Construct was implemented for the output type rather than the input type. The Construct approach was good because it allowed us to correlate from the desired type (ex: a Sprite component) to the "props" type (ex: SpriteProps). Note that "props" was the term I used for the "template" concept in my previous proposals. However the Construct approach was overly limiting: there were a number of desirable use cases where it resulted in the dreaded "conflicting implementations" error. Template lets us have our cake and eat it too. Multiple Templates can produce the same output type. The GetTemplate trait can be implemented for types that have a "canonical" Template, but we can always use raw templates directly in cases where a type cannot have a canonical template, shouldn't have one, or we want to override the canonical template.

}

impl SpawnScene for World {
fn spawn_scene<S: Scene>(&mut self, scene: S) -> EntityWorldMut {
Copy link
Member Author

Choose a reason for hiding this comment

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

What happened to efficiently spawning scenes "directly" without resolving to the dynamic format?

For awhile I supported this by unifying Scene and Template: Scene was implemented for any T: Template<Output: Bundle>. I then implemented Template for TemplatePatch and the SceneList impls, meaning that Scene was just a specialization of Template, and whether bsn! was a Template or a Scene was a matter of what trait you decided to return. Scene could then expose a spawn function.

I opted for a hard split between the traits (and the types) for the following reasons:

  1. This conflation resulted in a number of confusing / inconsistent behaviors:
    • When spawned as a template, patches would step on each other if they were defined twice. Ex: Player { x: 10. } Player { y: 10.} would behave as expected when spawned as a Scene, but they would step on each other when spawned as a Template (for performance reasons, patches returned a Bundle with the full patch type, preventing unnecessary archetype moves).
    • Attempting to spawn a bsn! that uses asset inheritance, directly as a template, would fail at runtime.
    • The more interestng "stateful" behaviors, such as patching inherited children, would also fail at runtime.
  2. It complicated the conceptual model. Users had to know "am I using the subset of bsn! that can be spawned directly"
  3. Templates that didn't return a Bundle needed specialized Scene impls to be used in that context.

I'm open to reconsidering this if we can address those concerns, or if we can't make the dynamic form suitably performant. Being able to directly / cheaply spawn a bsn! subset certainly has an appeal.


#[derive(Component, Debug, GetTemplate)]
struct Sprite {
handle: Handle<Image>,
Copy link
Member Author

Choose a reason for hiding this comment

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

What happened to "Template/Prop field syntax"?

This PR actually implements it! That being said, I am proposing we cut it, as it complicates the mental model, in practice it doesn't seem to be needed often, and in cases where it is, that functionality can be added manually to templates that need it. I really don't like that it forces manual user opt-in for each template to anticipate that actual value passthrough will be needed. If we decide inline handles should be supported in templates, we can add a HandleTemplate::Value(Handle<T>), and it will magically be supported everywhere without special bsn! syntax or manual opt-in.

This is what it looks like in the current system:

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

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

let handle = assets.load("player.png");
bsn! {
    Player {
        image: TemplateField::Value(handle),
    }
}

Sprite {
handle: "asset://branding/bevy_bird_dark.png",
size: 1,
nested: Nested {
Copy link
Member Author

Choose a reason for hiding this comment

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

Optional nested struct names?

In theory, we could omit nested struct type names:

// Type name specified
bsn! { Transform { translation: Vec3 { x: 1. } } }
// Type name omitted
bsn! { Transform { translation: { x: 1. } } }

However there are some issues:

  1. Unnamed field structs are ambiguous with tuple values

    foo: Foo(1, 2)
    foo: (1, 2)

    This is potentially resolvable by treating tuple values as patches too, as the field vs tuple accessors will be the same.

  2. Named field structs without fields specified are ambiguous with expression blocks

    foo: Foo {}
    foo: {}

    We could resolve this by just picking a winner (ex: empty expression blocks are actually empty struct patches, or vice versa). However that will make error cases (which will happen) potentially confusing for developers.

  3. Named field structs with fields require reaching further into the struct to disambiguate between expressions and struct patches.

    //   V V at both of the points marked with 'V' we don't know if this is an expr or a struct patch
    foo: { x: 1 }

    This is only a minor complexity issue from a parsing perspective as we can obviously look forward. However it has implications for autocomplete because when typing x we're still in a quantum superposition of "expression" or "field patch". We need to pick a winner, but a percentage of the time we will be wrong. In those cases, autocomplete will yield the wrong results, which will be painful and confusing for people.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should use a more explicit syntax {{ }} or ${ } for expression blocks.

> RegisterSystem<In> for IntoWrapper<I, In, Marker>
{
fn register_system(&mut self, world: &mut World) -> SystemId<In> {
world.register_system(self.into_system.take().unwrap())
Copy link
Contributor

@viridia viridia Jul 16, 2025

Choose a reason for hiding this comment

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

Does this leak? We need a way to unregister the callback when the owner is despawned.

This is why I had proposed using cached one-shots for inline callbacks, but we have to solve the type-erasure problem.

Copy link
Member Author

Choose a reason for hiding this comment

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

This does leak in its current form. This style of "shared resource" cleanup feels like it should be using RAII, as we discussed previously.

I'm not sure how the a cached one-shot would solve this problem, as it would still leak in that context as it doesn't use RAII?

From my perspective the only difference between this approach and register_system_cached is where the cache lives (inside the template, which is shared across instances, or inside world).

world.register_system_cached can be directly swapped in here for world.register_system if you think it would be better.

Copy link
Contributor

Choose a reason for hiding this comment

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

Because cached one-shots eventually get removed automatically, the Callback instance can be safely dropped without needing a destructor.

For the SystemId variant, we will need destruction, which I was attempting to solve with an ownership relation.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I understand the disconnect. We wouldn't use register_system_cached, that just returns an id. We'd use run_system_cached. That means that Callback retains ownership of the closure and is responsible for dropping it.

},
..Default::default()
position_type: PositionType::Absolute,
left: Val::Px(4.0),
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd really like to be able to say 4.0px here, or even 4px.

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'd like to include use Val::{Px, Percent} in the bevy_ui prelude, which would allow the still rust-ey but much shorter: left: Px(4.0) syntax.

Copy link
Contributor

Choose a reason for hiding this comment

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

There was also a proposal for an extension trait that would allow 4.0.px(), but tbh I prefer the Px(4.0) syntax.

Copy link
Member Author

@cart cart Jul 16, 2025

Choose a reason for hiding this comment

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

I particularly like that

  1. The "enum variant export" solution requires no special casing in the macro, meaning anyone (3rd party crates, app devs, etc) can utilize the pattern for their own units. This means everything will look / feel consistent.
  2. It works with RA go-to definition, autocomplete, doc hover, etc.

Copy link
Contributor

Choose a reason for hiding this comment

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

could .into() help turn Px(4.0) into Px(4) ?

label: C,
) -> impl Bundle {
(
pub fn checkbox(props: CheckboxProps) -> impl Scene {
Copy link
Contributor

Choose a reason for hiding this comment

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

What happened to the label?

Both checkboxes and radios contain a mix of iconic built-in children (the actual "box") and a parametric child (the label). This allows toggling by clicking on the label text, because otherwise checkboxes are an annoying small hit target.

Copy link
Member Author

Choose a reason for hiding this comment

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

The label is supplied by checkbox inheritors, if they so choose. Clicking still works as expected here, to my knowledge. I'm not getting annoyingly small hit targets in the feathers.rs example.

fn ui() -> impl Scene {
bsn! {
Node {
width: Val::Percent(100.0),
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to write 100pct or even 100%.

Choose a reason for hiding this comment

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

hmm. 100.0 seems standard IMO.

Copy link
Contributor

@Zeophlite Zeophlite Jul 16, 2025

Choose a reason for hiding this comment

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

Maybe alias Percent as Pct , along with removing Val:: , then you'd have Pct(100.0)

),
button(
ButtonProps {
on_click: Callback::System(commands.register_system(
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that commands.register_system() is leaky too - I was working on a solution for this.

),
}
[
:radio Checked::default() [(Text::new("One") ThemedText)],
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not understanding something here about the way the children lists are merged. Or at least, what's happening here doesn't match my naive intuition, I would have thought that [ ... ] would replace, rather than appending, children. This needs to be made clear in the 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.

I made the "append" behavior clear in the Inheritance section of the description. I agree that when the actual docs are written, this should be called out explicitly.

),
}
[
:radio Checked::default() [(Text::new("One") ThemedText)],
Copy link

@Runi-c Runi-c Jul 16, 2025

Choose a reason for hiding this comment

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

Is this Checked::default() instead of Checked in order to disambiguate it from relationship syntax? In the examples from the PR description, there are a few uses of e.g. Node [ ... ] that surprised me as well because it looks like it should be parsed as relationship syntax and thus fail because Node isn't a RelationshipTarget.

I think an explicit marker is needed to distinguish relationship syntax from patch + children shorthand, something like Children: [ ... ]?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup resolving this is at the top of my todo list. We discussed this in the working group Discord a few days ago.

@BenjaminBrienen BenjaminBrienen added D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Needs-Review Needs reviewer attention (from anyone!) to move forward S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged D-Macros Code that generates Rust code labels Jul 16, 2025
{children}
]
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

(posted here for threading, line of code irrelevant) In the PR description you write

It is generally considered best practice to wrap related entities with more than one entry in () to improve legibility. One notable exception is when you have one Template patch and then children:

bsn! {
    Node { width: Px(10.) }  [
        Node { width: Px(4.0) } [
            // this wraps with `()` because there are too many entries
            (
                Node { width: Px(4.0) }
                BackgroundColor(RED)
                [ Node ]
            )
        ]
    ]
}

What does this mean? I can't make heads or tails of this, this is an exception to wrapping for children because too many entries but then we wrap anyways? why is it too many entries? how many is too many? is this an optional "best practice" or is the exception that its mandatory?

Copy link
Member Author

@cart cart Jul 16, 2025

Choose a reason for hiding this comment

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

This is an optional best practice. Not mandatory. To help illustrate:

bsn! {
  Node { width: Px(10.) } [
    // This has one "entry", so no need to wrap with ()
    Node { width: Px(4.) },
    // This has two "entries", so we wrap with ()
    (Node { width: Px(4.) } BackgroundColor(RED)),
    // This has two "entries", but one is Node and the other is [], so we opt to not wrap it in ()
    Node { width: Px(4.) } [
        Node
    ],
    // We _could_ wrap it in () if we wanted, but I'm arguing that this does not improve
    // legibility, so we shouldn't include it in this case
    (Node { width: Px(4.) } [
        Node
    ])   
  ]
}

Copy link
Member

@BD103 BD103 Jul 16, 2025

Choose a reason for hiding this comment

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

Ohhh it's because () is used to group all components for a specific entity, while , is used to separate entities. It's because this:

bsn! {
    Human [
        // First child entity
        (
            Arm
            Health(100)
            Length(2)
        ),
        // Second child entity
        (
            Leg
            Health(75)
            Length(3)
        )
    ]
}

Is easier to read than this:

bsn! {
    Human [
        // First child entity
        Arm
        Health(100)
        Length(2),
        // Second child entity
        Leg
        Health(75)
        Length(3)
    ]
}

Copy link
Contributor

@IQuick143 IQuick143 Jul 16, 2025

Choose a reason for hiding this comment

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

bsn! {
    Human [
        Arm
        Health(100)
        Length(2),
        Leg
        Health(75)
        Length(3)
    ]
}

Is a little spooky syntax to me. Since a single comma (that's easy to miss!) somewhere can completely change the meaning.

Copy link
Contributor

Choose a reason for hiding this comment

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

bsn! {
    Human [
        Arm Health(100) Length(2),
        Leg Health(75) Length(3)
    ]
}

I hope we don't mega-bikeshed this one syntactic feature. There's ways to make it work either way.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this issue is particularly important as the entities get bigger - for smaller (dare I say toy) enties with simple components, you can segregate the entities by line, but as each child entitiy gets more complex, the risk increases.

Maybe we need a complex example to ruminate on?

Copy link
Contributor

Choose a reason for hiding this comment

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

With proper tooling, I don't think this would be a problem in practice. A formatter could use some heuristic to determine how many components is too many, and then place them in parentheses.

foo: 10
},
}
Gen::<usize> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you considered allowing omitting the :: in the turbofish as a syntax sugar? Not sure if it can be resolved in all cases but it seems like it should be possible

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 have. See my "MVP TODO" entry in the description :)
image

hash: u64, // Won't be serialized
name: Cow<'static, str>,
}
pub struct Name(pub HashedStr);
Copy link
Contributor

Choose a reason for hiding this comment

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

Some of these changes feel very uncontroversial and isolated/easy to land without even needing the greater bsn as justification like this one imo. Thoughts on landing bits like these early?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I think changes like this could land early.

ValorZard

This comment was marked as resolved.

@@ -145,6 +145,7 @@ default = [
"bevy_picking",
"bevy_render",
"bevy_scene",
"bevy_scene2",
Copy link
Member Author

Choose a reason for hiding this comment

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

@ValorZard I'm converting your top-level comment into a thread (please see the PR description):

(Not related to this comment at alll)
so this is probably out of scope from this PR, but how exactly will .bsn as a file format will work?
My original perception was that .bsn was basically JSX from React fused with the way Godot does scenes, and that seems to mostly be the case.
However, in the example you posted of the bsn! Macro having an observer callback, that’s just a regular rust function.
bsn! { Player on(|jump: On| { info!("Player jumped"); }) }
Will a .bsn file just have arbitrary rust code that runs during the game?

The idea is that .bsn will be the subset of bsn! that can be represented in a static file. In the immediate short term, that will not include things like the on function, as we cannot include arbitrary Rust code in asset files.

The primary goal of .bsn will be to represent static component values editable in the visual Bevy Editor. In the future we might be able to support more dynamic / script-like scenarios. But that is unlikely to happen in the short term.

Choose a reason for hiding this comment

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

Drat. I thought I did the comment thing correctly.

but otherwise that makes sense. So .bsn and bsn! Aren’t fully equivalent. Interesting.

I guess Bevy could support something like Godot’s callables in the future maybe, and have some sort of id sorted in the .bsn file that could be converted to a function? Idk

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, function reflection + a registry should make stringly-typed callbacks defined in assets very feasible.

Team::Green(10)
{transform_1337()}
Children [
Sprite { size: {4 + a}}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering why the expression syntax { } is required.

Copy link
Member

Choose a reason for hiding this comment

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

I originally thought it was because there aren't any commas to separate field assignments, but that actually isn't the case here since its a struct constructor.

use std::{any::TypeId, marker::PhantomData};
use variadics_please::all_tuples;

pub trait Scene: Send + Sync + 'static {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are Template and Scene separate types?

Could Template not just be expressed as patching an empty scene?

E.g. Godot has a single scene type.

handle: Handle<Image>,
}

#[derive(Component, Clone, Debug, GetTemplate)]
Copy link
Contributor

@JMS55 JMS55 Jul 16, 2025

Choose a reason for hiding this comment

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

Why does GetTemplate need to be manually derived?

The GetTemplate traits feels... weird to me. Creating a template from a type seems like an internal detail to me. As a bevy user, I just want to think in terms of entities, assets, components, and helper functions that return impl Scene. Ideally I just derive component on my type and can automatically use it in scenes/templates.

Can we not just blanket derive GetTemplate, and hide it as an internal detail?

Copy link
Member

Choose a reason for hiding this comment

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

I agree, GetTemplate here is more manual than I want it to be. Could we do something like:

impl<T: Template> GetTemplate for T::Output {
    type Template = T;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

That sadly goes directly against the point of GetTemplate.

The idea as I understand it, is that a Template is like a factory. And you can have several factories producing the same type of thing. So there's not a one-to-one correspondence between outputs and template types.

GetTemplate is there to express that sometimes there should be an obvious template for a type.
But I'm 99% sure this impl block is not valid, because it can and will create conflicting impls. (Even if it didn't create conflicting impls, rustc cannot prove it.)

Copy link
Contributor

Choose a reason for hiding this comment

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

We could make a ComponentTemplate<T: Component>(T) type that implements GetTemplate, and then in the bsn! macro, it could automatically wrap components in ComponentTemplate.

Copy link
Member Author

Choose a reason for hiding this comment

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

How would it know what Template logic to use for T: Component?

Copy link
Member Author

Choose a reason for hiding this comment

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

Also if you would like to try your hand at any reframings, you are welcome to! The best way to convince me there is a better approach is to make it happen. To keep the scope down / make experimentation easier just skip porting bsn! and compose scenes directly in Rust.

}
}

fn widget(children: impl SceneList) -> impl Scene {
Copy link
Contributor

Choose a reason for hiding this comment

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

impl Scene, or T: Scene works well for functions that return new scenes.

What are the options for storing a type erased scene/template(?) as a value?

E.g. I could see a use case where a plugin asks the user to provide something like a ResourceThing(Box<dyn Scene>), and then the plugin can use it to spawn scenes when needed.

Copy link
Member Author

Choose a reason for hiding this comment

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

Scene is object safe, so you can just box it directly. We could consider implementing Scene for Box<dyn Scene> if we decide that is necessary. In userspace, you can also already build a impl Scene for BoxedScene wrapper.

Templates are not object safe. There is an ErasedTemplate type used by ResolvedScene, but it is not usable as a Template directly. In theory it could be framed as a Template<Output = ()> though, if we decide that would be useful.

Copy link
Member

Choose a reason for hiding this comment

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

This is a long standing request of mine, see #3227. I would be extremely happy with a resolution in some form here.

@@ -58,14 +58,24 @@ struct Player {
move_cooldown: Timer,
}

#[derive(Default)]
struct Bonus {
entity: Option<Entity>,
i: usize,
Copy link
Member

Choose a reason for hiding this comment

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

(Code span unrelated.)

Linting BSN

I work on bevy_lint, and can write lints that help users migrate / enforce good practices with BSN. Do you have any ideas for lints that could help here?

The linter is able see basically anything the Rust compiler can, especially:

  • Syntax (before and after macro expansion)
  • Type and trait information

The linter works exactly the same as Clippy, so you can use that for further reference on our capabilities.

The linter doesn't work as well with formatting, however, so using it to auto-format bsn! invocations is mostly off the table.

cc @DaAlbrecht, who also works on the linter :)

use bevy_asset::{AssetPath, AssetServer, Assets};
use variadics_please::all_tuples;

pub trait SceneList: Send + Sync + 'static {
Copy link
Contributor

@JMS55 JMS55 Jul 16, 2025

Choose a reason for hiding this comment

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

Scene/bsn vs SceneList/bsn_list are confusing to me. If I understand correctly, the only difference is whether there's 1 or N root entities?

Is there no way to eliminate the separate type, and have the usage be inferred from context somehow? Why not just allow Scene to have N root entities, and if you use it somewhere it expects a single root entity it either wraps them in a new root entity or throws a runtime error?

Copy link
Member

@BD103 BD103 Jul 16, 2025

Choose a reason for hiding this comment

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

I agree. If we use commas to separate entities when declaring relations, why not always do it in the root of bsn!?

Copy link
Member Author

@cart cart Jul 16, 2025

Choose a reason for hiding this comment

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

I find it less confusing / more explicit. Is a Vec<X> the same thing as an X? They have different contexts and use cases. Conflating them moves the "oops you tried to inherit from a SceneList with len > 1" error to runtime instead of compile time. It certainly makes people not need to know about the bsn! vs bsn_list! nuance, but it also creates weird corner case behaviors in cases where people aren't aware of that nuance.

Functionally, I believe we'd still need the Scene vs SceneList trait distinction for Rust-type-system reasons. So the proposal would really be SceneList -> Scene, Scene->SceneEntry, bsn_list! -> bsn!, and the removal of the old bsn! implementation.

I also expect that this change would prevent the bsn! { Player {other_bsn} } case, as it would expect a SceneEntry impl in that position, not a Scene (previously know as SceneList) impl. Maybe resolvable though.

TokenStream::from(scene.to_tokens(&bevy_scene, &bevy_ecs, &bevy_asset))
}

pub fn bsn_list(input: TokenStream) -> TokenStream {
Copy link
Contributor

Choose a reason for hiding this comment

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

Graphs

I'm curious what your thoughts are on graph-like relationships in BSN. I know M:N relationships aren't yet supported, but I might get away with weaker links in the meantime.

Concretely, the new backend we're integrating in Better Audio processes audio through a DAG, where each node is an audio processor.

Currently, you can build the graph imperatively:

let sink = commands.spawn(VolumeNode::default()).id();

// source 1
commands
    .spawn(SamplerNode::default())
    .connect(sink);

// source 2
commands
    .spawn(SamplerNode::default())
    .connect(sink);

This produces a graph like:

┌────────┐┌────────┐
│source 1││source 2│
└┬───────┘└┬───────┘
┌▽─────────▽┐
│sink       │
└───────────┘

Since this graph has multiple roots, it can't be represented neatly like 1:N relationships.

Motivation

Describing audio graphs in BSN has big advantages, particularly when the asset format lands. As an asset, the graph could be hot-reloaded, tweaked by artists, or even serve as a serialized format for GUI editors.

Short-term solution

Since arbitrary DAGs can't be represented neatly as a simple hierarchy, I think it makes sense to reach for bsn_list!. Then, I could supply a M:N-relationships-at-home component on each node with output.

bsn_list! {
    (
        SamplerNode
        Connections [ #Sink ]
    ),
    (
        SamplerNode
        Connections [ #Sink ]
    ),
    (
        #Sink
        VolumeNode
    )
}

I suppose you'd make this a child of some other scene and then spawn it. A drawback of this approach is limited composability; if you wanted to insert two separate instances of the above graph fragment, the names would clash.

Name clashing could be resolved imperatively by passing in a unique identifier, maybe a UUID, and using that to describe edges. However, I don't see how that could work with the asset format.

If you have any thoughts on this, please let me know! I'm sure a complete solution is well outside the scope of this PR, but I'd love to hit the ground running with BSN and the better audio work.

Copy link
Member Author

Choose a reason for hiding this comment

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

It would definitely be cool to support this. Within a spawned hierarchy, do I think these types of graph references derive naturally from my proposed TODOs, namely these two:

  • Expand #Name syntax to be usable in EntityTemplate position for cheap entity references throughout the scene. This will likely involve replacing Template::build(entity: EntityWorldMut) with a wrapper TemplateContext, so we can cache these entity references.
  • Investigate adding related entities via a passed in entity reference (rather than spawning a new entity)

Based on the example you provided, it sounds like you came to the same conclusion :)

I suppose you'd make this a child of some other scene and then spawn it.

I see no reason why we couldn't also support a world.spawn_scene_list (with an accompanying ScenePatchList asset type) if you really want this to be flat.

A drawback of this approach is limited composability; if you wanted to insert two separate instances of the above graph fragment, the names would clash.

The names would clash, but FWIW my plan for "#MyName in EntityPath template position" is for it to be scoped to the currently spawned scene instance, so the references across multiple instances would resolve correctly / as expected. It would only become a problem if you had two peers with the same name and you try to access them with a normal "./MyName" path (which would resolve to the first match)

Comment on lines +25 to +28
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
Copy link
Contributor

@killercup killercup Jul 16, 2025

Choose a reason for hiding this comment

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

Is there a chance to have "variant shortcuts"? So that without any magical imports one could write

            width: Percent(100.0),
            height: Percent(100.0),
            align_items: Center,
            justify_content: Center,

here -- ideally with autocomplete, and without having to type/read the very stuttery align_items: AlignItems:: parts.

(Related to "struct shortcuts" https://github.com/bevyengine/bevy/pull/20158/files#r2209191853 and proposals for 100px/Px(100) https://github.com/bevyengine/bevy/pull/20158/files#r2209267145 syntax)

Copy link
Member Author

Choose a reason for hiding this comment

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

Sadly I don't think this is an option unless we add the variants to the current namespace (like the Px approach), but that would mean we can't have two Center variants from different enums.

This is because bsn! doesn't have access to type information. We can't express something like "if this field value is an enum, look up the type name and implicitly add it to the variant".

We could do something like: assume the field (ex: align_items)is an enum (obviously we can't do that), assume the field name corresponds directly to the type name, assume that type name is imported into the current namespace, convert the field name to CamelCase (ex: AlignItems), and append it to the supplied variant (ex: AlignItems::Center). But that breaks down at pretty much every step when generalized to arbitrary fields :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, we could add prefixed aliases:

pub use AlignItems::Center as AlignCenter;
pub use JustifyContent::Center as JustifyCenter;

pub enum AlignItems {
    Center,
}

pub enum JustifyContent {
    Center,
}

type Template = EntityPath<'static>;
}

impl<T: Clone + Default> Template for T {
Copy link
Contributor

Choose a reason for hiding this comment

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

The Default bound doesn't seem to actually be used here. Are you expecting to use T::default() for something, or was this just to avoid conflicts with other impls? And, in either case, does it make sense to generalize it to FromWorld?

Copy link
Member

Choose a reason for hiding this comment

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

See also my [comment above] about "do we still even need FromWorld" too.

@alice-i-cecile alice-i-cecile added the M-Needs-Release-Note Work that should be called out in the blog due to impact label Jul 16, 2025
@alice-i-cecile alice-i-cecile added this to the 0.18 milestone Jul 16, 2025
@@ -145,6 +145,7 @@ default = [
"bevy_picking",
"bevy_render",
"bevy_scene",
"bevy_scene2",
Copy link
Member Author

Choose a reason for hiding this comment

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

@3xau1o I'm converting your top level comment to a thread (please read the PR description)

can read mentions to Diffing for BSN

If the framework cannot accommodate a given approach in its current form (ex: coarse diffing)

Explore dynamic field-based lists for patches, which would BSN diffing better / more granular

Does it mean that BSN is using a slower v-dom like diffing reactivity approach in the spirit of react instead of a faster fine-grained reactivity system in the spirit of svelte5/solidjs/vue-vapor?

Is it possible to see the reasoning behind this when there is a tendency of moving towars fine-grained updated instead of brute diffing?

examples of recent libraries UI rewritten/moved from partial diffing to full fine grained reactivity include svelte 5 and vue vapor

a good reference is solidjs design

BSN does not currently support reactivity (please read the PR description). There have been many investigations into both fine grained and coarse reactive solutions (both diff-ing and signal-based) / we are well versed in the space at this point. The goal for this phase is to (if possible) build BSN in such a way that it can support both paradigms. Then we can iterate / have an ecosystem (potentially even cross-compatible) where ideas can compete. From there if a winner arises, we can bless it as the "go-to" / default solution.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can confirm there's multiple people in the community who I know are planning to experiment with different approaches. I'm looking forward to building a coarse-grained diffing system, and I'll bet @viridia will build a fine-grained solution.

I'm really pleased with this reactivity-agnostic approach, let's us avoid a big bikeshed and do more incremental exploratory work.

Choose a reason for hiding this comment

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

On one hand:

build BSN in such a way that it can support both paradigms ... where ideas can compete

On the other hand:

We want to define scenes as patches of Templates.

Can a diff-based system be the foundation of an efficient implementation of fine-grained reactivity? The suggestion seems to be that this is supposed to be a neutral foundation, but I'd argue that it is not. You can't build a (true) reactive system on top of a diff-based system without many compromises.

Copy link
Contributor

@NthTensor NthTensor Jul 16, 2025

Choose a reason for hiding this comment

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

I think you may be confusing two different things: Inheritance and reactivity. Patches are just for inheritance. Personally, I don't see how they relate to reactivity (or incrementalization) at all.

Choose a reason for hiding this comment

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

@NthTensor Uhh... Gotcha. They're more like layers then, right? Maybe the confusing "patch" terminology could be avoided.

Copy link

@3xau1o 3xau1o Jul 17, 2025

Choose a reason for hiding this comment

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

build BSN in such a way that it can support both paradigms ... where ideas can compete

this is only possible if BSN is only a DSL syntax like JSX which is used in both Reactjs and Solidjs
that will leave BSN as a tree description tool rather that a composable component system

there is also the need to realize that

  • fine grained reactivity is mostly compile time
  • diffing is mostly runtime

they're so different, mostly opposite, that's why Vue.js vapor was mostly a rewrite instead of an upgrade from Vue.js 3 vdom, Vue Vapor and Vue3 are not compatible, they only use the same vue syntax, same as React and Solidjs use JSX

@bevyengine bevyengine deleted a comment from 3xau1o Jul 16, 2025
}
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 {
Copy link
Contributor

@NthTensor NthTensor Jul 16, 2025

Choose a reason for hiding this comment

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

I am particularly pleased and impressed with the enum support, I know they can be tricky to handle in these sorts of macros. The enum update approach makes sense to me, and I'm interested in playing with it.


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

What role does FromWorld play in a post-GetTemplate Bevy? I would prefer if it was completely replaced: I don't like the arbitrary mutable side effects.

But this is just a gut feeling now: there may be barriers I'm missing.

Copy link
Contributor

Choose a reason for hiding this comment

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

What about Resources?
If I haven't missed anything, BSN can't currently instantiate them.

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 be able to instantiate resources via templates, especially in a resources-as-components Bevy. I agree though, it doesn't look like it's currently implemented.

}

fn ui() -> impl Scene {
bsn! {
Copy link
Contributor

@KirmesBude KirmesBude Jul 16, 2025

Choose a reason for hiding this comment

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

It makes sense to focus on the bsn! macro, but could we also get a snippet what the code would need to look like e.g. for the player example?

Gen::<usize> {
value: 10,
}
on(|event: On<Explode>| {
Copy link
Member

Choose a reason for hiding this comment

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

Oh! This isn't special-cased! That's very nice.

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`].
pub trait Template {
Copy link
Member

Choose a reason for hiding this comment

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

Can we currently spawn resources via templates?

If no, are you planning to allow that? Is that blocked on resources-as-components?

Copy link
Member Author

Choose a reason for hiding this comment

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

We could have a ResourceTemplate that has no effect on the entity in question and just inserts the Resource (and handles conflicts in whatever way it sees fit). I think this might be better suited to the BSN Set scenario, where we could make resources their own "type" in the set. But theres nothing stopping us from experimenting with the ResourceTemplate approach.

type Output;

/// Uses this template and the given `entity` context to produce a [`Template::Output`].
fn build(&mut self, entity: &mut EntityWorldMut) -> Result<Self::Output>;
Copy link
Member

@alice-i-cecile alice-i-cecile Jul 16, 2025

Choose a reason for hiding this comment

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

Just so I understand the broader design: if we have templates and scenes (which can contain multiple entities), why do we need bundle effects, which primarily allow bundles to contain multiple entities?

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 A-UI Graphical user interfaces, styles, layouts, and widgets C-Feature A new feature, making something new possible C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Complex Quite challenging from either a design or technical perspective. Ask for help! D-Macros Code that generates Rust code M-Needs-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 S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.