Skip to content

场景组合与 Patch

BSN 的核心设计目标之一是组合:把小场景拼成大场景,同时只覆盖需要改的字段。

场景函数

用普通 Rust 函数返回 impl Scene,就是可复用的场景片段:

rust
fn enemy() -> impl Scene {
    bsn! {
        Health { current: 100, max: 100 }
        Damage(10)
    }
}

Listing 32-3(节选):enemy() 场景函数

enemy() 返回一个带 Health { current: 100, max: 100 }Damage(10) 的场景。调用 world.spawn_scene(enemy()) 就能生成一个普通敌人。

字段级 Patch

组合场景时,只需写想覆盖的字段。未提及的字段保留原值:

rust
fn boss() -> impl Scene {
    bsn! {
        enemy()
        Health { max: 500, current: 500 }
        Damage(50)
    }
}

Listing 32-3(节选):boss() 组合并覆盖字段

boss() 先包含 enemy() 的全部内容,再用 Health { max: 500, current: 500 }Damage(50) 覆盖。最终 boss 的属性是 Health { current: 500, max: 500 }Damage(50)

完整示例:

rust
fn setup(world: &mut World) {
    // 普通敌人:Health { current: 100, max: 100 }, Damage(10)
    world.spawn_scene(enemy()).unwrap();

    // Boss:从 enemy() 组合,覆盖 Health 和 Damage
    world.spawn_scene(boss()).unwrap();

    let mut query = world.query::<(&Health, &Damage)>();
    for (health, damage) in query.iter(world) {
        println!("  HP {}/{}, 伤害 {}", health.current, health.max, damage.0);
    }
}

Listing 32-3(节选):生成普通敌人和 Boss

运行:

console
cargo run -p ch32-scenes --example listing-32-03
text
  HP 100/100, 伤害 10
  HP 500/500, 伤害 50

多次 Patch 合并

同一组件可以被多次 patch,效果叠加:

rust
bsn! {
    Button                 // 插入默认值
    Button { width: 200 }  // 只改 width
    Button { height: 300 } // 只改 height
}

最终 Button { width: 200, height: 300 }——两次 patch 各自生效,互不干扰。

Patch 的工作原理

bsn! 对每个组件条目生成一个 Template。当同一个组件类型出现多次时,后面的 Template 会合并到前面的 Template 上——只有显式设置的字段会被覆盖,未设置的字段保留原值。这和"整个组件替换"的普通 ECS 行为不同。

合并顺序:从左到右、从上到下。后面的 patch 覆盖前面的。

组合的传递性

场景函数可以嵌套调用:

rust
fn base() -> impl Scene {
    bsn! { Health { current: 100, max: 100 } }
}

fn warrior() -> impl Scene {
    bsn! {
        base()
        Damage(20)
    }
}

fn elite_warrior() -> impl Scene {
    bsn! {
        warrior()
        Health { max: 300 }
        Damage(40)
    }
}

elite_warrior() 最终有 Health { current: 100, max: 300 }Damage(40)——base()current 保留,warrior()Damage(20) 被覆盖。