Bevy Scene Notation (BSN)

BSN 是 Bevy 0.19 落地的声明式场景语法层,基于 bsn! / bsn_list! 宏和 SceneComponent derive。它是 cart 在 Discussion #14437 中提出的四大支柱的最后一块拼图:

  1. Required Components(0.15 已落地)
  2. Construct / GetTemplate Trait
  3. Scenes and Patches(格式无关的场景补丁系统)
  4. BSN(人体工学的声明式语法)

相关页面

核心特性

bsn! 宏 — 声明式组件清单

将命令式的 commands.spawn((...)).with_children(...) 转为类似配置清单的写法:

fn button(label: &str) -> impl Scene {
    bsn! {
        Button
        Node { width: px(150), height: px(65), justify_content: Center, align_items: Center }
        BorderColor::from(Color::BLACK)
        BorderRadius::MAX
        BackgroundColor(Color::srgb(0.15, 0.15, 0.15))
        Children [(Text(label) TextFont { font_size: 33.0 } TextColor(Color::WHITE))]
    }
}

旧语法(已废弃)— : 前缀的「继承」时代

⚠️ 2026-05-21 #24367 已废弃此语法。 以下仅为历史参考,新代码请使用上方「Prefix Overhaul」章节的语法。

先展开模板,再覆盖特定字段(已废弃的旧写法):

// ⚠️ 旧语法(已废弃,仅供参考)
bsn! {
    :button("Cancel")                    // : 被误称为「继承」,实际是 cacheable
    BackgroundColor(Color::srgb(0.4, 0.15, 0.15))
}

迁移到新语法:~button("Cancel") 并配合新的 SceneComponent 消歧 @SceneComponent

Prefix Overhaul(0.19, #24367)

2026-05-21 合并的 #24367 (laundmo) 重新设计了 BSN 前缀系统,消除了令人困惑的「继承」概念。^[https://github.com/bevyengine/bevy/pull/24367]

功能旧语法新语法
Template patching@Template~Template
SceneComponent 消歧:SceneComponent@SceneComponent
缓存标记: (被误称为「继承」): (正式称为 “cacheable”)
缓存的 SceneComponentN/A: @SceneComponent

设计理由: : 作为「继承」的说法不准确——不带 : 的 scene 产生相同结果。: 的真正含义是”请缓存此场景”。将 : 语义简化为纯缓存标记后,SceneComponent 需要新前缀,于是从 Template 那里取走了 @,引入 ~ 给 Template patching。

术语更新: “inheritance” → “include”/“included”。

缓存尚未启用: 本 PR 仅做语法准备,实际缓存功能未开放。cart 明确说缓存带参数的 scene function 暂不支持(需要参数到缓存场景的映射、hashable 约束等)。

Review 注意事项: Zeophlite 指出 ~Thing 和未来可能的 -Thing(移除补丁)视觉上太相似,建议后续考虑不同符号。cart 推送了小修改后批准。

对所有 BSN 代码有破坏性: 每个使用 : + SceneComponent 或 @ + Template 的 bsn! 调用都需要语法迁移。

SceneComponent — 出厂配置

#[derive(SceneComponent)] 让组件自带一份子实体/组件配置,spawn 时自动展开:

#[derive(SceneComponent, Default, Clone)]
struct Player {
    score: usize,
}
 
impl Player {
    fn scene() -> impl Scene {
        bsn! {
            Transform::default()
            Children [LeftHand, RightHand]
        }
    }
}

使用时写 @Player { score: 0 }(新语法)而非直接写 Player { score: 0 }。只有带 @ 前缀的形式会展开 scene()

world.spawn_scene(...) — 一行拍进 World

fn setup(world: &mut World) {
    world.spawn_scene(scene());
}

EntityTemplate 传递(0.19, #24174)

BSN 现支持将 #Name 实体引用作为 EntityTemplate 传递给 scene function 或 @props,实现跨 scene 的实体引用组合。

#[derive(Component, FromTemplate)]
struct Reference(Entity);
 
fn widget(entity: EntityTemplate) -> impl Scene {
    bsn! {
        Reference(entity)
    }
}
 
bsn! {
    #SomeName
    Children [
        ~widget(#SomeName),        // ~ 模板 patching(新语法)
        foo(#{some_entity_value}), // 表达式求值为 Entity
        @Bar {
            @myprop: #SomeName     // @ SceneComponent(新语法)
        }
    ]
}

支持四种传递方式:~fn(#Name)(模板 patching)、fn(#Name)fn(#{expr})@prop: #Name

争议:合并时标记 X-Contentious,但未公开记录具体反对意见。争议详情需查阅 review 讨论。

后续:#24336 (bsn! const/unsafe block 支持) 已于 2026-05-20 合并入 main,进入 0.19。

const/unsafe block 支持(0.19, #24336)

BSN 现支持在 bsn! 宏的字段表达式中使用 constunsafe 块:

fn friendly(people: &[&'static str]) -> impl Scene {
    bsn! {
        Friend {
            name: const {"John"}
            // SAFETY: Jesus take the wheel
            father: unsafe {people.get_unchecked(0)}
        }
    }
}

async 块被移除: 初始提案包含 async 支持,但因类型系统限制被砍掉 — async future 产生不可命名类型(voldemort types),无法满足生成 struct 的 Clone + Default 约束。用户可手动 Box::new(async{...}) 绕行。

技术债务: cart 评价为 “A bit of a hack”,长期计划(post-0.19)是将 bsn! 位置值视为普通 Rust 表达式处理。

Scene Functions — 可复用场景函数

BSN 场景可以作为函数定义和复用,支持参数传递:

fn player(name: &str) -> impl Scene {
    bsn! {
        Name(name)
        Player
        Children [ Sword, Shield ]
    }
}

返回值类型 impl Scene 是标准模式,编译器确保返回的是合法 BSN 场景。

Scene Spawning Systems — .spawn() 一键转系统

任何返回 SceneSceneList 的函数都可以通过 .spawn() 直接转为 Bevy 系统:

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, level.spawn())
        .run();
}
 
fn level() -> impl SceneList {
    bsn_list![
        Camera2d,
        Sprite { image: "player.png" }
    ]
}

不再需要手动写 fn setup(mut commands: Commands, ...) 把场景函数包一层。

Scene Lists — 多实体列表

bsn! / Scene 对应单个实体。bsn_list! / SceneList 对应多个实体的列表:

fn players() -> impl SceneList {
    bsn_list! [
        (#Player1 Team::Blue),
        (#Player2 Team::Red),
    ]
}

实体之间逗号分隔,括号(表示实体边界)是可选的。SceneList 可以作为参数传入场景函数:

fn widget(children: impl SceneList) -> impl Scene {
    bsn! {
        Widget
        Children [ {children} ]
    }
}

BSN Relationships — 一等关系支持

BSN 对 ECS Relationships 提供一等支持。除了 Children [],也支持自定义关系类型:

bsn! {
    Player
    Inventory [
        Apple,
        Potion,
    ]
}

此处 Inventory 是自定义的 ECS relationship 组件。与 Children 不同,Inventory 使用自定义关系语义(不会自动 despawn 子实体等)。

可组合 Patch — 场景层层叠加

BSN 表达式本质上是 patch,不会写入类型的”完整”实例。这意味着场景可以层层叠加,每层只覆盖关心的字段:

fn button() -> impl Scene {
    bsn! {
        Button
        Node { width: px(100) }
    }
}
 
fn my_button() -> impl Scene {
    bsn! {
        button()
        Node { height: px(100) }
    }
}

my_button spawn 时得到 Node { width: px(100), height: px(100) }。底层 button() 初始化 width,上层 my_button 追加 height。

Templates 与 FromTemplate

BSN 表达式实际定义的是组件的”模板”(Template)而非组件本身。Template 可以访问 World、当前实体、场景 spawn 上下文。

FromTemplate trait 告诉 BSN 某个 Component 对应什么 Template 类型。可以 derive:

#[derive(Component, FromTemplate)]
struct Reference(Entity);

大多数类型不需要手动实现 FromTemplate — 实现了 Default + Clone 的类型会自动获得”自身作为模板”的实现。只有当需要模板特性时(如 Sprite { image: "player.png" } 将字符串路径自动转为 Handle<Image>)才需要 derive。

与旧 Bundle 写法的对比 — 旧代码需要传入所有 ECS 依赖:

// 旧写法
fn player(asset_server: &AssetServer) -> impl Bundle {
    (
        Player { score: 10, ..Default::default() },
        children! [
            Sprite {
                image: asset_server.load("player.png"),
                ..Default::default()
            }
        ]
    )
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(player(&asset_server))
}

BSN 不需要手动传递依赖:

// BSN
fn player() -> impl Scene {
    bsn! {
        Player { score: 10 }
        Children [ Sprite { image: "player.png" } ]
    }
}
fn setup(mut commands: Commands) {
    commands.spawn_scene(player());
}

Inline Asset Templates — 内联创建资产

asset_value() 模板允许在 BSN 中内联创建资产,无需预先通过 Res<Assets<T>>

fn cube() -> impl Scene {
    bsn! {
        Mesh3d(asset_value(Cuboid::new(1., 1., 1.)))
    }
}

对比旧写法需要手动管理 Assets<Mesh> resource 和 handle:

// 旧写法
fn setup(mut meshes: ResMut<Assets<Mesh>>) -> impl Bundle {
    let handle = meshes.add(Cuboid::new(1., 1., 1.));
    Mesh3d(handle)
}

Entity Reference Syntax — #Name 作用域与图结构

#Name 语法在 BSN 中定义实体的 Name 组件,等效于 Name("Name")。此外,#Name 可在同一个 bsn!{} scope 内任意位置被引用:

bsn! {
    References {
        child: #Child,
        grandchild: #Grandchild,
    }
    Children [
        #Child Children [
            #Grandchild
        ]
    ]
}

bsn_list! 中,这可以定义图结构:

bsn_list! [
    (#A PointsTo(#B)),
    (#B PointsTo(#A)),
]

此处 PointsTo 是一个接受 Entity 参数的组件。

Implicit Into — 字段位置自动转换

BSN 字段位置的值会自动 .into() 为目标类型,无需手动转换:

#[derive(Component, Default, Clone)]
struct Foo(String);
 
bsn! {
    Foo("hello")   // &str 自动 Into<String>
}

UI 场景中尤其有用:

// 旧写法
Node {
    border: UiRect::all(Val::Px(2.0))
    ..Default::default()
}
 
// BSN
Node { border: px(2) }

px(2) 产生 Val::Px(2.0)UiRectimpl Into<UiRect> for Val 产生 UiRect::all。这不是硬编码的特殊转换,而是标准的 Rust trait 派生,可自定义。

事件观察 — 内联回调

BSN 实体可以内联观察事件,在场景中嵌入回调行为:

fn button() -> impl Scene {
    bsn! {
        Node { width: px(100), height: px(50) }
        on(|press: On<Pointer<Press>>| {
            info!("button pressed!")
        })
    }
}

on() 函数将闭包转为 observer,在事件触发时执行。

Scene Assets & Caching — 场景资产与缓存

BSN 在 0.19 已准备就绪支持场景资产依赖,只是不含官方 .bsn 加载器(计划下版本推出):

commands.queue_spawn_scene(bsn! {
    :"player.bsn"
    Transform { translation: Vec3 { x: 10. } }
})

: 前缀启用缓存:首次加载 "player.bsn" 后缓存结果,后续复用无需重复解析。queue_spawn_scene 会等待所有依赖加载完毕再 spawn;spawn_scene 立即尝试 spawn,依赖未加载则失败。

glTF 加载器也将移植到新场景系统(支持 :"my_scene.gltf" 语法)。

Option<P> 条件场景(#24536)

Option<P> 实现了 Scene(其中 P: Scene),支持零分配条件 spawn:

let optional_component = if condition {
    Some(bsn! { Foo })
} else {
    None
};
 
world.spawn_scene(bsn! {
    #MaybeFoo
    {optional_component}
});

OptionNone 时什么都不做。此前需要用 Box<dyn Scene>,引入堆分配和类型擦除。

SmallVec 作为 SceneList(#24542)

SmallVec<[S; N]> 实现了 SceneList,可直接用于 BSN 场景列表而无需先转 Vec

同时实现了异质版本 SmallVec<[Box<dyn SceneList>; N]> 支持混合类型场景列表。

Ready event — 场景完全就绪信号(0.20, #25296)

2026-08-05 合并的 #25296(milestone=0.20, M-Release-Note, +166/-21)为场景系统补上”完全 ready”事件:所有依赖加载完、完整层级存在、初始组件全部插入后触发——与现有 Add 事件(自上而下、子实体不可用)互补的”自下而上”时机。

动机: 构建自包含、可组合场景的关键拼图;也是把 Bevy 逻辑叠加在 glTF 等外部场景表示之上的必要前提(此前只能用 Add 事件做 top-down 初始化,子实体尚不可用)。

迁移含义: 依赖”完整场景”的后处理逻辑(如绑定子实体引用、初始化跨层级关系)应从 Add 监听迁移到新 Ready 事件。

0.19 子集局限

限制说明时间线
无法导出/序列化不能像旧系统那样写入 .scn.ron 文件0.20
GLTF 未移植GLTF 加载仍需 WorldAssetRoot0.20
.bsn 资产格式只能写在 Rust 代码里0.20
debug symbol 膨胁单个 symbol 可达 121KB,栈使用 62KB已提交优化
负数常量 Bugrust-analyzer 会对宏内负数常量报错,但 cargo build 能过Issue #24050
静态检查未实现@Player 前缀遗漏只有运行时 error log0.20

从 Bundle 到 BSN 的演进

阶段版本关键机制解决了什么
Bundle 时代0.1–0.14#[derive(Bundle)]类型安全的组件组合,但样板代码爆炸
Required Components0.15#[require(Node, UiImage)]组件自描述依赖
BSN 子集0.19bsn! + SceneComponent声明式语法 + 层级 + cacheable 缓存
完整 BSN0.20.bsn 资产格式 + writer + 静态检查外部文件、编辑器管线、编译期安全

社区参考

  • 官方示例: examples/scene/bsn.rs
  • Feathers UI 工具集已通过 PR #23536 完成 BSN 移植
  • 社区模板系统: i-cant-believe-its-not-bsn (Leafwing-Studios)

决策树

  • 纯 UI / 2D 原型 → main 分支的 BSN 已经可用
  • 需要场景序列化 / GLTF / 编辑器 → 继续用 bevy_world_serialization(旧系统)等 0.20
  • debug build 编译慢 → 等优化进稳定版

相关页面