Composable scene authoring for Bevy, defined using the Bevy Scene Notation (BSN) format.
Game entities rarely exist in isolation. A 3D level might be made up of walls, floors, props and enemies. A 2D character might need a distinct sprite entity for weapon, hat and boots. A UI popup might need text and multiple buttons for accept, cancel, minimize and close actions. Spawning these collections as individual, disjointed entities is tedious, error-prone, and hard to reuse. A scene lets you describe a conceptual object, made of an entity, its components, children, and assets, once and spawn it wherever you need it.
Any scene system must overcome three challenges:
- Composability: combining smaller scenes into larger ones without duplicating shared constants and setup code.
- Granular overrides: when reusing a scene, overriding individual fields on a component (like changing just a button's width) without having to respecify every other field on that component.
- Asset integration: referencing assets (meshes, textures, sounds) from within scenes without manually wiring up asset handles.
This crate tackles all three, via [Scene] composition, Template-based
field-level patching, and automatic string-to-asset-handle resolution.
The [bsn!] macro exposes these ideas, and makes the process of scene-authoring pleasant
by providing a terse syntax for defining [Scene]s inline.
This brevity is essential: making it easier to review and understand scenes at a glance,
resolve merge conflicts and keep file sizes under control.
The macro includes best-effort Rust-Analyzer support. Autocomplete, go-to-definition, and hover docs
should work inside the macros, and this effort should transfer over correctly to other LSPs!
BSN syntax reference
For a quick rundown on how to read and write BSN syntax, see the docs for [bsn!].
Quick Start
Spawn entities in a [Scene] by calling World::spawn_scene,
wrapping a call to the [bsn!] macro.
# use App;
# use ;
# use *;
# use AssetPlugin;
# use TaskPoolPlugin;
# let mut app = new;
# app.add_plugins;
# let world = app.world_mut;
;
;
;
// #Player adds a `Name("Player")` component to the root entity.
// Children spawns two child entities: one with Sword, one with Shield.
world.spawn_scene;
Core Concepts
- [
Scene]: Describes what a spawnedEntityshould look like, created using [bsn!] or, in the future,.bsnasset files. Conceptually, a [Scene] contains a list of "entries" to apply to anEntity. - [
SceneList]: A list of scenes, returned by [bsn_list!]. Each [Scene] in the list produces oneEntity. - Scene Composition: Composition works by including scenes in other scenes. The included scenes "entries" will be treated as if they were written in the outer scene.
Template: ATemplateis something that, given a spawn context (targetEntity, [World], etc), can produce some output. Think of it as a "superpowered ECS-aware constructor" for a type. In the context of scenes,Templates are used to produce [Component]s and [Bundle]s. This enables defining scenes without needing to pass in a bunch of their dependencies (such as assets). TheFromTemplatetrait is used to associate some final output type (ex: a [Component]) with a canonicalTemplatethat produces it.FromTemplate/Templateis automatically implemented for types that implement [Default] + [Clone], which is generally preferred. You should manually deriveFromTemplatewhen a type needs custom template logic (ex: one of its fields is an "asset handle", which has custom template logic).- [
RelatedScenes]: These add a [SceneList] as related to this [Scene] by a specificrelationship. This kind of change is added to the [Scene] by specifying aRelationshipTargetcomponent like [Children], followed by a [SceneList].
Spawning Scenes
There are two approaches to spawning scenes:
- Immediate:
World::spawn_sceneandCommands::spawn_sceneresolve and spawn in one step. Returns an error if any asset dependencies are not yet loaded. - Queued:
World::queue_spawn_sceneandCommands::queue_spawn_sceneregister the scene's dependencies and wait for them to load before resolving and spawning. When the dependencies are loaded (or there are no dependencies), the scene will spawn during that frame's [SpawnScene] schedule, betweenUpdateandPostUpdate.
In all cases, your *_spawn_scene method call should wrap an invocation of the [bsn!] macro,
or call a function which returns a [Scene].
See the [WorldSceneExt], [CommandsSceneExt], [EntityWorldMutSceneExt], and
[EntityCommandsSceneExt] extension traits for the full set of scene-spawning APIs.
Entity Hierarchies and Relationships
Use Children [scene1, scene2] inside [bsn!] to spawn child entities.
[Children] (and entities within [bsn_list!]) are separated by commas;
add multiple components to the same entity by listing them without a comma:
// Spawns one child entity with components A, B and C
bsn! { #Parent Children [A B C] }
// Spawns two child entities, one with A and B, the other with C, due to the added comma
bsn! { #Parent Children [A B, C] }
// Spawns two child entities, but more clearly separated due to parentheses.
bsn! { #Parent Children [(A B), C] }
These invocations can be nested to build deeper hierarchies.
bsn! {
#Parent
Children [
#Child1 SomeComponent,
#Child2
SomeComponent
Children [
#GrandChild1 SomeComponent,
#GrandChild2
]
]
}
We can improve clarity at the cost of compactness through the careful use of newlines, parentheses and indentation:
bsn! {
#Parent
Children [
(
#Child1
SomeComponent
),
(
#Child2
Children [
(
#GrandChild1
SomeComponent
),
(
#GrandChild2
)
]
),
]
}
This is fundamentally a stylistic choice: white space, Rust comments (// and /* */), and parentheses used in this way are ignored.
The tools discussed here are not limited to [Children]: any RelationshipTarget type can be used the same way.
Named Entity References
The #Name syntax assigns a Name to an entity and registers it for cross-referencing within the same macro invocation.
Within the same bsn! invocation / scope, it is possible to reference an entity by its #Name, generating an EntityTemplate which ultimately resolves to an Entity:
bsn! {
#Name
my_scene(#Name)
ComponentA(#Name)
ComponentB { entity: #Name }
Children [
ComponentC(#Name)
]
}
Notice that the "child entity" was able to access the parent entity via #Name. It is also possible for ancestors to access
their descendants:
bsn! {
#Root
Children [
Reference(#Root)
]
}
Using #Name as a value in [bsn!] will result in an EntityTemplate, which is a Template that resolves to an Entity
[Component]s with Entity fields should generally derive FromTemplate, because Entity uses FromTemplate to map to EntityTemplate.
Scope rules
Each [bsn!] invocation creates its own name scope. A name is visible to the root
entity, its children, and any deeper descendants in the same call. The reverse is also
true: descendants can "look up" the hierarchy.
Composed scenes (via my_scene(#Name)) or SceneComponents
each contain their own [bsn!] invocation and therefore their own scope,
so re-using the same name across multiple different scenes is fine.
However, the results of a named entity reference, the EntityTemplate,
can be passed to other scenes. It is valid only during the spawning of a scene.
That means Components should never store EntityTemplate fields,
they should store the resolved Entity instead and
derive FromTemplate to convert EntityTemplate automatically.
If both a parent and a composed child define the same name (e.g. both use #X),
each scope's #X resolves to its own entity, avoiding conflicts or potentially unintuitive shadowing.
In a [bsn_list!], all root entities share a single name scope, so sibling scenes
can reference each other by name. This is useful for wiring up relationships between
entities that are spawned together. For example, a group of UI panels where each
panel needs a relationship to its neighbor:
fn linked_pair() -> impl SceneList {
bsn_list![
(#Left Link(#Right)),
(#Right Link(#Left)),
]
}
Dynamic Name Values and Entity References
#SomeName syntax will set the value of the Name component to Name("SomeName"), and make the entity reference-able in [bsn!]. #Name syntax is always scoped and
doesn't support "dynamic" names. If you would like to both reference an entity in [bsn!] and provide a dynamic name, you can do this:
let i = 0;
bsn! {
#Root
Name({format!("Entity {i}")})
Children [
Reference(#Root)
]
}
Adding Name("desired name") after the #SomeName reference will patch over the Name component created by the reference to give it a custom name.
Patching
When you insert a component into an Entity in normal ECS code, the entire pre-existing value is replaced.
If a scene sets Button { width: 100, height: 300 } and a caller wants to
change just width, ordinary component insertion would force them to respecify height too.
Patching avoids this. When you write Button { width: 200 } in [bsn!], it creates
a patch that sets only the width field. Unmentioned fields keep their existing values
(from a included scene, an earlier patch, or the type's defaults). Multiple patches to the
same component and its values are applied in order, only overwriting the fields they changed.
The following scenes all end up with a button which is 200 wide and 300 high.
impl Default for Button {
fn default() -> Self {
Button { width: 100, height: 300 }
}
}
bsn! { Button { width: 200, height: 300 } } // fully specified
bsn! { Button { width: 200 } } // only changing width, height defaults to 300
bsn! {
Button // inserts defaults
Button { width: 200 } // changes width
Button { height: 300 } // changes height
}
Required Traits
To make a component available in [bsn!], derive either [Default] + [Clone], or FromTemplate.
Both support patching: unmentioned fields keep their values from earlier patches or the
type's defaults, and multiple patches merge rather than overwrite.
The distinction is about what values a field can hold at spawn time:
- [
Clone] + [Default] (e.g.#[derive(Component, Default, Clone)]): covers the simple case, and should be your default choice. FromTemplate(e.g.#[derive(Component, FromTemplate)]) is needed when a field requires spawn-time context. Examples includeHandle<T>fields which needAssetServerto resolve asset paths, orEntityfields which resolveEntityTemplates from named entity references. If any of your fields' types implementFromTemplatemanually / have custom template logic, you should derive it for the parent type as well if you want your type to use that logic.
Deriving FromTemplate and [Default] on the same type is not allowed, as both would supply a FromTemplate impl and conflict.
FromTemplate derivers still have access to a default constructor of sorts though: the derive generates a companion struct
for YourType named YourTypeTemplate which implements Default, so YourTypeTemplate::default() serves the same purpose.
Enums in bsn
Enums are special-cased to allow for better implicit defaults: [bsn!] requires that enums have defaults for all variant arms, not just the type as a whole.
When [bsn!] encounters a Enum, it will try to get the default value for the variant using static methods like default_{variant_lower}.
To help with setting up these methods, theres a pseudo-derive called VariantDefaults.
It works like a normal derive macro, but without a matching Trait. It just generates a impl block with the default_{variant_lower} static methods.
Deriving FromTemplate also implies/works like VariantDefaults.
Composition
Composition relies on patching to work nicely, allowing you to include other scenes in the current ones. All of their patches will be applied at the position they're included.
Example:
# use App;
# use ;
# use *;
# use AssetPlugin;
# use TaskPoolPlugin;
# let mut app = new;
# app.add_plugins;
# let world = app.world_mut;
// Include `enemy()` and patch just the `max` field:
world.spawn_scene;
The spawned entity has Health { current: 100, max: 200 }: the max field is overridden
while current retains the value from enemy(). Tuples of [Scene]s also implement
[Scene], so patches from multiple sources merge into a single [ResolvedScene].
For programmatic patching outside of [bsn!], see the [PatchFromTemplate] and
[PatchTemplate] traits.
Scene Caching
Note: Caching is currently only implemented for scene assets. It hasn't yet been wired up for "function scenes" or [SceneComponent]s. Attempting to use
it in those cases will result in a compile error.
Scenes can be cached, improving performance. Since this can change the semantics in some cases, this requires an explicit opt-in.
Caching works by resolving the included scene and storing the resulting [ResolvedScene] for future use. When the outer scene is spawned again,
it will not need to resolve the included scene again, instead patching on top of the cached version (using copy-on-write semantics for each Template).
This means caching can only be used if the scene is the first scene entry.
This scene includes an uncached "enemy" scene:
bsn! {
enemy()
Health { max: 200 }
}
This scene caches the "enemy" scene by adding the : prefix (however caching scene functions like this is not currently supported)
bsn! {
:enemy
Health { max: 200 }
}
Scene assets always need to be cached using the : prefix.
Note that the .bsn file format is not yet released. (This already works, assuming theres a loader for the asset format)
bsn! {
:"enemy.bsn"
Health { max: 200 }
}
Loading Assets into Scenes
Without the use of scenes, loading an asset requires referencing the AssetServer explicitly:
let handle: Handle<Image> = asset_server.load("player.png");
commands.spawn(Sprite { image: handle, ..default() });
This can be particularly frustrating when defining helper functions for spawning entities,
which require you to pass AssetServer or handles through multiple layers of function calls.
In [bsn!], asset paths work directly as field values. When a component field is a
Handle<T>, the [bsn!] macro accepts a string literal in its place. Under the hood,
this creates a HandleTemplate that calls AssetServer::load at resolve time.
If the asset has already been loaded, this returns the existing handle rather than
loading it again.
commands.spawn_scene(bsn! {
Sprite { image: "player.png" }
});
A [Component] must also derive FromTemplate to accept asset paths:
#[derive(Component, FromTemplate)]
struct Icon {
image: Handle<Image>,
tint: Color,
}
// "icon.png" is converted to a HandleTemplate<Image> via implicit .into()
commands.spawn_scene(bsn! {
Icon { image: "icon.png", tint: Color::WHITE }
});
Observers
Use on() inside [bsn!] to attach an entity [Observer]. Entity observers are closures or
functions which fire when a given EntityEvent is triggered and targets this entity.
The first parameter's type determines which event is observed. Multiple observers can be added to
the same entity, and the observer has full access to the ECS via system parameters:
#[derive(EntityEvent)]
struct Damage {
entity: Entity,
amount: u32,
}
#[derive(EntityEvent)]
struct Heal {
entity: Entity,
amount: u32,
}
fn player() -> impl Scene {
bsn! {
Health { max: 100, current: 100 }
// Each `on(...)` attaches a separate observer.
on(|damage: On<Damage>, mut query: Query<&mut Health>| {
let mut health = query.get_mut(damage.entity).unwrap();
health.current = health.current.saturating_sub(damage.amount);
})
on(on_heal)
}
}
fn on_heal(heal: On<Heal>, query: Query<&mut Health>){
let mut health = query.get_mut(heal.entity).unwrap();
health.current = (health.current + heal.amount).min(health.max);
}
This is useful for self-contained logic like click handlers, damage reactions,
or scripting-style triggers.
Closures passed to [on] work like any Rust closure:
you can use move and capture variables from the enclosing scope normally.
Using Dynamic Expressions in Scenes
The [bsn!] macro is not limited to static data. Because scene functions are plain
Rust functions, you can accept parameters and capture variables from the enclosing scope.
Use {...} (curly braces) anywhere a value is expected to embed an arbitrary Rust expression:
fn enemy(hp: u32, name: &str) -> impl Scene {
let name_string = name.to_string();
bsn! {
#{name}
Health { current: {hp / 2}, max: hp }
Sprite { image: {name_string + ".png"} }
}
}
// Call it like an ordinary Rust function
commands.spawn_scene(bsn! { enemy(200, "goblin") });
Braces are required when the macro would otherwise misparse the expression
and for complex expressions like {hp * 2}.
Dynamic template values
A Template value, such as an instance of a Component, cannot be directly passed in to a bsn! block, as bsn!
expects "scene variables" in that position. Instead use template_value(...) which accepts a given component Template value
and returns a [Scene] implementation for it.
fn enemy(translation: Vec3){
let transform = Transform::from_translation(translation);
bsn! {
#Foo
template_value(transform)
}
}
Ad-hoc template functions
Sometimes you need custom behavior or world access to create a Template.
If this is the case, you can use template instead of a custom FromTemplate or Template implementation.
In template you get access to a TemplateContext which
contains the [EntityWorldMut] and a collection of named entity references.
bsn! {
#Foo
template(|ctx| {
Foo(ctx.resource::<MyAssetCollection>().get("generated_asset_name"))
})
}
Expressions as scenes
You can insert a [Scene] or [SceneList] in another Scene using curly-bracketed expressions:
fn container(contents: impl SceneList) -> impl Scene {
bsn! {
Children [
#Header,
{contents},
#Footer,
]
}
}
let items = bsn_list![#A, #B, #C]; // or bsn! if container takes a `impl Scene`
commands.spawn_scene(container(items));
Conditional values
There is no if/match syntax inside the [bsn!] grammar (yet!), but you can embed
conditionals via {...} blocks or handle them outside the macro:
fn unit(is_boss: bool) -> impl Scene {
let hp = if is_boss { 500 } else { 100 };
bsn! { Health { current: hp, max: hp } }
}
One way to achieve conditional scenes is using a [Box<dyn Scene>] to store different scenes in one variable.
fn unit(is_boss: bool, level: u32) -> impl Scene {
let scene: Box<dyn Scene> = if is_boss {
Box::new(bsn! {
Boss
Followers [ // the boss is followed by some grunts
:unit(false, level - 1) #Grunt1,
:unit(false, level - 2) #Grunt2
]
})
} else {
Box::new(bsn! { Grunt })
};
bsn! {
Level(level)
{scene}
}
}
We plan on making "conditional scenes" easier to define in future releases.
Scene Components
A [SceneComponent] is a specialized type of [Component] that has an associated [Scene]:
# use *;
# use *;
#
# ;
#
# ;
This enables including the [SceneComponent] as a scene, using the following syntax:
# use bevy_scene::prelude::*;
# use bevy_ecs::prelude::*;
# #[derive(SceneComponent, Default, Clone)]
# struct Player {
# score: usize
# }
# impl Player {
# fn scene() -> impl Scene {}
# }
# let mut world = World::new();
world.spawn_scene(bsn! {
@Player { score: 0 }
});
This will spawn the Player component and the entire scene with it. This means that you write
systems that query for the Player component, they can generally assume the rest of the scene will be there
too!
[SceneComponent]s can only be spawned using scene APIs like World::spawn_scene. Spawning
them using [World::spawn] will log an error.
Custom Scene Functions
When deriving [SceneComponent], it defaults to using Self::scene as the "scene function".
Scene functions can also be manually specified:
# use *;
# use *;
;
SceneComponent Asset Paths
Note: Currently, Bevy does not include a .bsn asset format. These docs exist to help you understand what is planned, and what is currently possible
with third-party asset formats.
Alternatively, a scene asset path can be specified:
# use *;
# use *;
Scene Components are Template-able
Just like other [Component]s, [SceneComponent]s are "template-able"
# use bevy_scene::prelude::*;
# use bevy_ecs::{prelude::*, template::TemplateContext};
# let mut world = World::new();
# struct Handle<T>(std::marker::PhantomData<T>);
# struct HandleTemplate<T>(String, std::marker::PhantomData<T>);
# impl<'a, T> From<&'a str> for HandleTemplate<T> {
# fn from(value: &'a str) -> Self { todo!() }
# }
# impl<T> Default for HandleTemplate<T> {
# fn default() -> Self { todo!() }
# }
# struct Image;
# impl<T> Template for HandleTemplate<T> {
# type Output = Handle<T>;
# fn build_template(&self, context: &mut TemplateContext) -> Result<Handle<T>> { todo!() }
# fn clone_template(&self) -> Self { todo!() }
# }
# impl<T> FromTemplate for Handle<T> {
# type Template = HandleTemplate<T>;
# }
#[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" }
});
SceneComponent 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":
# use bevy_scene::prelude::*;
# use bevy_ecs::prelude::*;
# #[derive(Component, Default, Clone)]
# struct Node;
# #[derive(Component, Default, Clone)]
# struct Text(String);
# let mut world = World::new();
/// 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" when the scene is included in another scene.
This means that they are not "patchable", as at that point they have already been evaluated,
and they produce "patchable" outputs.
You can set both props and normal fields at the same time:
# use bevy_scene::prelude::*;
# use bevy_ecs::prelude::*;
# let mut world = World::new();
# impl Widget {
# fn scene(props: WidgetProps) -> impl Scene {}
# }
#[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,
}
});
The Scene Component is Always Added
Specifying the scene component manually in the scene function is not necessary. It will be added automatically:
# use *;
;
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 fields:
# use *;
#
# ;
#
#
#
Scene Components vs Required Components
At first glance, Scene Components and Required Components solve similar problems. They both provide a mechanism to initialize components with other components.
They are functionally quite different however. It is worth understanding the differences and tradeoffs:
- Required Components: Context-less (ex: Default constructors), non-hierarchical, can always be applied immediately, not dependency aware, automatically enforced at runtime as components are added, not patchable, pretty low overhead, not a lot of features / functionality
- Scene Components: Require context (ex: World access and "Entity Spawn Context", such as entity references), hierarchical (spawn children), cannot always be applied immediately (can have dependencies that aren't loaded yet), dependency aware, only enforced at spawn time, patchable, more dynamic / higher overhead, many features.
Some good rules of thumb:
- Are you building something "hierarchical" / with related entities? Use [
SceneComponent]. - Do you want or need the full capabilities of the scene system? Use [
SceneComponent]. - Are you spawning something that has dependencies / needs World access? use [
SceneComponent]. - Are you defining "flat" components that aren't really scenes on their own? Use required components.
- Do you need the "required" components to be automatically added in non-scene contexts? Use required components.
- Is spawn performance a very high priority? Use required components.
.bsn Asset Format
Bevy does not currently have support for .bsn files,
but intends to offer a .bsn asset format in future releases.
This would allow you to define your scenes on disk, creating/modifying them in various authoring tools and using asset hot-reloading.
This format is intended to have broad syntactic compatibility with the bsn! macro,
making it easy to port your content between both the macro and the asset form.
When planning your future use of .bsn asset files (which are not currently shipped), be aware that
unlike bsn! macro calls .bsn assets will not support expressions or other dynamic features directly.
For now, you should use existing non-Bevy asset formats like glTF,
search for ecosystem implementations or stick to bsn! macro calls.
Note that the architecture to support an asset format already exists, allowing community implementations/experimentation until an official version exists. An example of how to go about this can be found in the scene benchmarks