Skip to content

Scene Components#24008

Merged
cart merged 16 commits into
bevyengine:mainfrom
cart:scene-components
May 1, 2026
Merged

Scene Components#24008
cart merged 16 commits into
bevyengine:mainfrom
cart:scene-components

Conversation

@cart

@cart cart commented Apr 27, 2026

Copy link
Copy Markdown
Member

Objective

It is essentially a Bevy rite of passage to define a Player component and then ask "ok when I spawn Player, how do I spawn all of the other entities / components / scenes it needs to function with it?".

In Bevy, components are the "unit of behavior". The presence of a component indicates that the entity should behave a certain way. Scenes are inextricable from behaviors, yet currently there is no way to tie Components (bevy's unit of behavior) to scenes. This is a fundamental gap in the Bevy data model. It is a problem that developers expect their engine to solve for them.

It is high time that we had an official solution to this problem. BSN was built to be that solution, and it is now time to do the last-mile work to facilitate the Component <-> Scene relationship

Solution

It is now possible to define "scene components", where SceneComponent: Component:

#[derive(SceneComponent, Default, Clone)]
struct Player {
    score: usize
}

impl Player {
    fn scene() -> impl Scene {
        bsn! {
            Transform { translation: Vec3 { x: 10. } }
            Children [
                LeftHand,
                RightHand,
            ]
        }
    }
}

This enables inheriting from the Player component as a scene:

world.spawn_scene(bsn! {
    :Player { score: 0 }
})

After the inherited Player scene component is spawned, the entire scene is available. This means that you can Query<&Player> in your ECS systems, and trust that the entire scene is also present.

Inheritance Syntax vs Patch Syntax

Notice that this uses inheritance syntax in BSN (:), rather than normal "component patch" syntax (ex: bsn! { Player { score: 0 } }. Semantically these are different things:

  • Scene inheritance syntax: constructs the full scene and inherits from it
  • Component patch syntax: Just patches the component directly and creates it if it doesn't exist. This will not do any scene inheritance. You can still patch scene components this way as long as the scene component is inherited somewhere "earlier" in the inheritance hierarchy.

Attempting to spawn a scene component on its own without the scene (ex: commands.spawn(Player::default())) will log an error, as Scene Components should never exist without their scene.

Custom Scene Functions

When deriving SceneComponent, it defaults to using Self::scene as the "scene function". Scene functions can also be manually specified:

#[derive(SceneComponent, Default, Clone)]
#[scene(player)]
struct Player {
    score: usize
}

fn player() -> impl Scene {
   bsn! { /* scene here */}
}

Inherit BSN Asset Paths

Alternatively, a scene asset path can be specified:

#[derive(SceneComponent, Default, Clone)]
#[scene("player.bsn")]
struct Player {
    score: usize
}

Once we port the glTF loader to BSN, this will also be supported:

#[derive(SceneComponent, Default, Clone)]
#[scene("player.gltf")]
struct Player {
    score: usize
}

Scene Props

Sometimes it is desirable to "parameterize" a scene: pass in values to the scene which determine what the scene outputs are.

The answer to this in BSN is "scene props":

/// A UI widget that repeats "hello" text a given number of times.
#[derive(SceneComponent, Default, Clone)]
#[scene(HelloRepeaterProps)]
struct HelloRepeater;

#[derive(Default)]
struct HelloRepeaterProps {
    repeat: usize,
}

impl HelloRepeater {
    fn scene(props: HelloRepeaterProps) -> impl Scene {
        let hellos = (0..props.repeat)
            .map(|_| bsn!{ Text("hello") })
            .collect::<Vec<_>>();
        bsn! {
            Node
            Children [
                {hellos}
            ]
        }
    }
}

world.spawn_scene(bsn! {
   :HelloRepeater {
       @repeat: 5
   } 
});

Notice the @field syntax, which specifies that a prop is being set instead of a field. Props are evaluated "immediately" at the point of inheritance where the scene is constructed. This means that they are not "patchable".

You can set both props and normal fields at the same time:

#[derive(SceneComponent, Default, Clone)]
#[scene(WidgetProps)]
struct Widget {
    value: usize
}

#[derive(Default)]
struct WidgetProps {
    border: bool,
}

world.spawn_scene(bsn! {
   :Widget {
       @border: true,
       value: 10,
   } 
});

Scene props can be used with templates:

#[derive(SceneComponent, Default, Clone)]
#[scene(PlayerProps)]
struct Player;

#[derive(Default)]
struct PlayerProps {
    head: AssetPath<'static>,
    body: AssetPath<'static>,
}

impl Player {
    fn scene(props: PlayerProps) -> impl Scene {
        bsn! {
            Transform
            Visibility
            Children [
                (Head, Sprite { image: {props.head} }),
                (Body, Sprite { image: {props.body} }),
            ]
        }
    }
}

world.spawn_scene(bsn! {
   :Player {
       @head: "big_head.png"
       @body: "small_body.png"
   } 
});

Scene Components are Template-able

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

impl Player {
    fn scene() -> impl Scene {
        bsn! { /* scene here */}
    }
}

world.spawn_scene(bsn! {
   :Player { image: "player.png" } 
});

The Scene Component is Always Added

Specifying the scene component manually in the scene function is not necessary. It will be added automatically:

#[derive(SceneComponent, Default, Clone)]
struct Player {
    score: usize
}

impl Player {
    fn scene() -> impl Scene {
        bsn! {
            // No need to specify a Player component here.
            // It is implied!
        }
    }
}

However you can patch the scene component in the scene if you would like. This comes in handy if you would like props to contribute to the scene component's value:

impl Player {
    fn scene(props: PlayerProps) -> impl Scene {
        bsn! {
            Player {
                size_in_meters: {props.size_in_millimeters / 1000. }
            }
        }
    }
}

SceneComponent Trait

This is what SceneComponent looks like under the hood:

pub trait SceneConstructor: FromTemplate<Template: Default>
where
    <Self::Template as Template>::Output: Component,
{
    type Props: Default;
    fn scene(props: Self::Props) -> impl Scene;
}

In general, developers should prefer #[derive(SceneComponent), which derives this trait automatically, ensures the Component is never spawned without its scene, and automatically initializes the Component even if it isn't specified explicitly in the user-defined scene function.

Reusable Component Derive Logic

This PR moves the component derive logic into a new bevy_ecs_macro_logic crate and refactors it to be reusable and extendable.

This enables the SceneComponent derive to reuse and configure the Component derive. I also took the liberty of porting the redundant Resource derive logic to this system, as it follows the same pattern.

PR Todo

  • Port this PR description to docs on Scene.

Whats Next

  • Scene Component scene caching: It might make sense to cache unparameterized scenes (like we do for scene assets) to cut down on spawn costs. We could even have an opt-in parameterized scene cache (ex: hash the props).
  • Reactivity: It is important to note that props are not reactive. They do not exist post-spawn (aka "at runtime") and they cannot be changed. However they could easily be part of a reactivity story:
    • Props as components: insert props as components and re-run the scene constructor when they are mutated
    • Signal props: Reactive "signal" types could be passed down the init hierarchy via props.
  • Simpler "prop patching" codegen: currently for simplicity props reuse the template-patching logic, but this introduces a few constraints and complexities that are unnecessary. We should consider writing specialized logic for them.

@cart cart added this to the 0.19 milestone Apr 27, 2026
@cart cart added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Scenes Composing and serializing ECS objects labels Apr 27, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS Apr 27, 2026
@cart
cart force-pushed the scene-components branch from e1f7849 to 3c46ed5 Compare April 27, 2026 22:28
@cart
cart force-pushed the scene-components branch from 3c46ed5 to 6d37222 Compare April 27, 2026 22:43
@bevyengine bevyengine deleted a comment from Diddykonga Apr 28, 2026
Comment thread crates/bevy_scene/src/lib.rs
@alice-i-cecile alice-i-cecile added S-Needs-Review Needs reviewer attention (from anyone!) to move forward D-Complex Quite challenging from either a design or technical perspective. Ask for help! X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers labels Apr 28, 2026

@NicoZweifel NicoZweifel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I like the idea of a SceneComponent and macro but I really don't like making it part of the ECS and feature gating it. To me a component and the ECS is and should not be aware of what a scene is.

In Bevy, components are the "unit of behavior".

Could you elaborate? Why is this in quotes? I was under the impression systems are the behavior in an ECS, operating on the data (components).

Ofc you can encapsulate logic onto components but that's opinionated and not inherently an ECS/Bevy thing the way I see it.

I would heavily recommend solving this through architecture and not using feature flags for this since it introduces circular dependency traps among other annoyances (editors can have all flags, depending on config). Personally I dont think this is what features should be used for and this is clearly a bandaid that affects devex negatively.

TL;DR: I think its a good idea overall but I dont think it belongs in the ecs.

Comment thread crates/bevy_ecs/macros/src/component.rs Outdated
Comment thread crates/bevy_ecs/macros/src/lib.rs Outdated
Comment thread crates/bevy_scene/src/scene.rs Outdated
Comment thread crates/bevy_scene/src/scene.rs Outdated
Comment thread crates/bevy_scene/src/scene.rs Outdated
Comment thread crates/bevy_scene/src/scene.rs Outdated
Comment thread crates/bevy_scene/src/scene.rs Outdated
@bevyengine bevyengine deleted a comment from NicoZweifel Apr 28, 2026
Comment thread crates/bevy_feathers/src/controls/color_plane.rs Outdated
Comment thread crates/bevy_scene/macros/src/bsn/parse.rs Outdated
Comment thread crates/bevy_scene/src/scene_list.rs Outdated
Comment thread crates/bevy_scene/macros/src/bsn/codegen.rs Outdated
@cart

cart commented Apr 30, 2026

Copy link
Copy Markdown
Member Author

I believe this is "ready". I've opted not to implement the "static" enforcement of disallowing world.spawn(SomeSceneComponent) because it is just invasive / disruptive enough to existing generic spawning workflows that I'd like to do it on its own time / in the 0.20 cycle.

Comment thread crates/bevy_scene/src/scene_component.rs

@alice-i-cecile alice-i-cecile left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I really like the improved API and the better separation of concerns. Much better; I'm glad we took the time to get this right.

However, I was promised docs!

/// See the "Scene Components" sections of the [Scene] docs to see how this is used in practice.

Three complaints here:

  1. I think this should be in the crate docs: the rest of our high-level explanation and usage context goes here.
  2. Those docs should go over the relation to required components, at least briefly.
  3. I think you forgot to actually add those docs :) Checking Scene on this branch does not reveal any new docs.

@Diddykonga Diddykonga left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Excellent 👍
The Component Derive cleanup is nice, still might be able to add Resource as an attribute to the Component derive but that's future work.

@NicoZweifel NicoZweifel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for resolving 👍

@Trashtalk217 Trashtalk217 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I did a once over for the ECS stuff and it looks good. I like that it's sufficiently well seperated from the Scene stuff.

Attempting to spawn a scene component on its own without the scene (ex: commands.spawn(Player::default())) will log an error, as Scene Components should never exist without their scene.

This is definitely a good usecase for archetype invariants, which we should look into.

@cart
cart enabled auto-merge May 1, 2026 02:29
@cart
cart added this pull request to the merge queue May 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 1, 2026
@alice-i-cecile
alice-i-cecile added this pull request to the merge queue May 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 1, 2026
@cart
cart added this pull request to the merge queue May 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 1, 2026
@cart
cart added this pull request to the merge queue May 1, 2026
@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 1, 2026
Merged via the queue into bevyengine:main with commit aadbf46 May 1, 2026
44 checks passed
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in ECS May 1, 2026
villejuutila added a commit to villejuutila/bevy that referenced this pull request May 2, 2026
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 Composing and serializing ECS objects 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! S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants