Bevy Scene Notation (BSN)
BSN 是 Bevy 0.19 落地的声明式场景语法层,基于 bsn! / bsn_list! 宏和 SceneComponent derive。它是 cart 在 Discussion #14437 中提出的四大支柱的最后一块拼图:
- Required Components(0.15 已落地)
- Construct / GetTemplate Trait
- Scenes and Patches(格式无关的场景补丁系统)
- BSN(人体工学的声明式语法)
相关页面
- bevy — Bevy 引擎主页
- bsn-spawn-button-20-to-5 — 归档 | Bevy BSN:spawn 按钮从 20 行到 5 行
核心特性
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”) |
| 缓存的 SceneComponent | N/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! 宏的字段表达式中使用 const 和 unsafe 块:
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() 一键转系统
任何返回 Scene 或 SceneList 的函数都可以通过 .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),UiRect 有 impl 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}
});当 Option 为 None 时什么都不做。此前需要用 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 加载仍需 WorldAssetRoot | 0.20 |
无 .bsn 资产格式 | 只能写在 Rust 代码里 | 0.20 |
| debug symbol 膨胁 | 单个 symbol 可达 121KB,栈使用 62KB | 已提交优化 |
| 负数常量 Bug | rust-analyzer 会对宏内负数常量报错,但 cargo build 能过 | Issue #24050 |
| 静态检查未实现 | @Player 前缀遗漏只有运行时 error log | 0.20 |
从 Bundle 到 BSN 的演进
| 阶段 | 版本 | 关键机制 | 解决了什么 |
|---|---|---|---|
| Bundle 时代 | 0.1–0.14 | #[derive(Bundle)] | 类型安全的组件组合,但样板代码爆炸 |
| Required Components | 0.15 | #[require(Node, UiImage)] | 组件自描述依赖 |
| BSN 子集 | 0.19 | bsn! + SceneComponent | 声明式语法 + 层级 + cacheable 缓存 |
| 完整 BSN | 0.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 编译慢 → 等优化进稳定版
相关页面
- bevy — Bevy 引擎实体
- bevy-projects — ZoOL 的 Bevy 项目看板
- bevy-game-development — Bevy/Rust 游戏开发概念
- bevy-migration-notes — 升级注意事项
- bevy-version-notes — 版本速览