Skip to content

状态作用域实体:自动清场

OnEnter(Playing) 里生成的勇者和史莱姆,是这一局的人。一局结束回到菜单,他们就该退场。先看看没人管退场会怎样——开局生成两个角色,别的什么都不做:

rust
/// 开局:生成本局的登场角色
fn spawn_actors(mut commands: Commands) {
    println!("  [OnEnter(Playing)] 勇者与史莱姆登场");
    commands.spawn((Actor, Name::new("勇者")));
    commands.spawn((Actor, Name::new("史莱姆")));
}

Listing 10-6(其一):只管生,不管收

每帧清点画面上的角色:

rust
/// 清点画面上的角色
fn stage_report(state: Res<State<GameState>>, actors: Query<&Name, With<Actor>>) {
    let roster: Vec<_> = actors.iter().map(Name::as_str).collect();
    if roster.is_empty() {
        println!("  画面({:?})上空无一人", state.get());
    } else {
        println!(
            "  画面({:?})上站着 {} 个角色:{}",
            state.get(),
            roster.len(),
            roster.join("、")
        );
    }
}

Listing 10-6(其二):清点系统

剧本是一场败仗:投币、Game Over 回菜单、不服气再投一币。运行:

console
cargo run -p ch10-states --example listing-10-06
text
—— 第 1 帧 ——
  罗兰投币。(叮)
  画面(Menu)上空无一人
  [OnEnter(Playing)] 勇者与史莱姆登场
—— 第 2 帧 ——
  画面(Playing)上站着 2 个角色:勇者、史莱姆
—— 第 3 帧 ——
  屏幕:史莱姆扑倒勇者——GAME OVER,退回待机画面。
  画面(Playing)上站着 2 个角色:勇者、史莱姆
—— 第 4 帧 ——
  画面(Menu)上站着 2 个角色:勇者、史莱姆
—— 第 5 帧 ——
  罗兰:不服,再来!(叮)
  画面(Menu)上站着 2 个角色:勇者、史莱姆
  [OnEnter(Playing)] 勇者与史莱姆登场
—— 第 6 帧 ——
  画面(Playing)上站着 4 个角色:勇者、史莱姆、勇者、史莱姆
—— 第 7 帧 ——
  老板:打烊喽。
  画面(Playing)上站着 4 个角色:勇者、史莱姆、勇者、史莱姆

两处病灶,一处比一处重:第 4 帧,待机画面上站着上一局没散场的勇者和史莱姆;第 6 帧,新一局的 OnEnter 又生成一套,两个勇者对两只史莱姆,场面彻底失控。这就是开篇说的“幽灵实体”:生成有挂载点,退场全靠自觉。

按第 8 章的思路,你可能想到在 OnExit(Playing) 里写个清场系统——能行,但每写一类“局内实体”都得记着把它纳入清单。Bevy 的做法更顺手:让实体自己声明归属

DespawnOnExit:把退场写在出生证上

修复只需要在生成时多挂一个组件:

rust
/// 开局:生成本局的登场角色,离开 Playing 时自动注销
fn spawn_actors(mut commands: Commands) {
    println!("  [OnEnter(Playing)] 勇者与史莱姆登场");
    commands.spawn((Actor, Name::new("勇者"), DespawnOnExit(GameState::Playing)));
    commands.spawn((
        Actor,
        Name::new("史莱姆"),
        DespawnOnExit(GameState::Playing),
    ));
}

Listing 10-7:挂上 DespawnOnExit,离开 Playing 时引擎自动清场

DespawnOnExit(state) 是个普通组件,意思是:世界离开 state 的那一刻,把我连同子树一起 despawn。再跑一遍同样的剧本:

console
cargo run -p ch10-states --example listing-10-07
text
—— 第 1 帧 ——
  罗兰投币。(叮)
  画面(Menu)上空无一人
  [OnEnter(Playing)] 勇者与史莱姆登场
—— 第 2 帧 ——
  画面(Playing)上站着 2 个角色:勇者、史莱姆
—— 第 3 帧 ——
  屏幕:史莱姆扑倒勇者——GAME OVER,退回待机画面。
  画面(Playing)上站着 2 个角色:勇者、史莱姆
—— 第 4 帧 ——
  画面(Menu)上空无一人
—— 第 5 帧 ——
  罗兰:不服,再来!(叮)
  画面(Menu)上空无一人
  [OnEnter(Playing)] 勇者与史莱姆登场
—— 第 6 帧 ——
  画面(Playing)上站着 2 个角色:勇者、史莱姆
—— 第 7 帧 ——
  老板:打烊喽。
  画面(Playing)上站着 2 个角色:勇者、史莱姆

第 4 帧空无一人,第 6 帧恰好一套。几条边界,把这个组件用稳:

  • 清场发生在转换期间,与 OnExit(Playing) 同一阶段、且保证在 OnEnter(新状态) 运行之前落地——新状态的搭台系统看到的一定是清过的场子。
  • despawn 是第 9 章的那个 despawn:带 Childrenlinked_spawn 关系的,整棵子树一起走。给“局内场景”的根实体挂一个 DespawnOnExit,整棵树就托管了。
  • 同值转换不清场。上一节手肘撞出的 Playing → Playing 会重跑 OnExit/OnEnter,但 DespawnOnExit 按兵不动——于是 OnEnter 里的 spawn 会再来一套。要让“重开本关”干净利落,用不同的状态值绕一圈,或者别依赖同值转换重开。
  • 还有个反向的兄弟 DespawnOnEnter(state):进入某状态时清。常见用法是挂在“游戏结束”画面的遗留物上——进入 Menu 就扫掉。

“OnEnter 搭台 + DespawnOnExit 挂牌”就是 Bevy 管理阶段性实体的标准搭配:生成处声明归属,退场全自动,加多少种局内实体都不用维护清单。第 20 章的 Breakout 会原样复用这套搭配。

到这里,单层状态机的全套工具齐了。但真实游戏的状态往往不止一层——“暂停”就不是和“菜单/游戏中”平级的阶段,它只在游戏中才有意义。下一节给状态分层。