Latest commit 1d3f6e4

状态

相关的官方文档:[state]


状态可以控制应用程序的运行时“流程”。状态可以做这些事:

  • 制作菜单画面或加载画面
  • 暂停(或取消暂停)游戏
  • 不同的游戏模式
  • ...

不同的状态可以运行不同的系统。还可以添加一次性(one-shot)设置和清理(cleanup)系统,以便在进入或退出某个状态时执行。

要使用状态,先要定义一个枚举类型,并把 [System Sets] 添加到 [App Builder]:

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum AppState {
    MainMenu,
    InGame,
    Paused,
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)

        // 添加 `AppState` 类型
        .add_state(AppState::MainMenu)

        // 默认添加的系统运行在所有状态下(与状态无关)
        .add_system(play_music)

        // 这个系统在“更新” `AppState::MainMenu` 状态时运行
        .add_system_set(
            SystemSet::on_update(AppState::MainMenu)
                .with_system(handle_ui_buttons)
        )

        // 这个系统在“进入” `AppState::MainMenu` 状态时运行
        .add_system_set(
            SystemSet::on_enter(AppState::MainMenu)
                .with_system(setup_menu)
        )

        // 这个系统在“离开” `AppState::MainMenu` 状态时运行
        .add_system_set(
            SystemSet::on_exit(AppState::MainMenu)
                .with_system(close_menu)
        )

        .run();
}

同一状态可以有多个系统集,这样放置[标签]和使用[显式系统排序]时更方便。

状态对[插件]也有用,每个插件都可以把自己的系统集添加到同一个状态中。

状态是用 [Run Criteria] 来实现的。这些特殊的系统集构造函数实际上只是帮助程序自动添加状态管理运行条件(todo:不理解)。

控制状态

在系统里,可以用 State<T> 资源来检查和控制状态:

#![allow(unused)]
fn main() {
fn play_music(
    app_state: Res<State<AppState>>,
    // ...
) {
    match app_state.current() {
        AppState::MainMenu => {
            // TODO: play menu music
        }
        AppState::InGame => {
            // TODO: play game music
        }
        AppState::Paused => {
            // TODO: play pause screen music
        }
    }
}
}

切换到另一个状态:

#![allow(unused)]
fn main() {
fn enter_game(mut app_state: ResMut<State<AppState>>) {
    app_state.set(AppState::InGame).unwrap();
    // ^ 如果当前状态已经是目标状态,或者已经有另一个“切换状态”在排队,会报错
}
}

当前状态的所有系统都执行完毕后,Bevy 会转换到下一个状态。

同一帧更新中可以做多次状态转换,Bevy 会处理所有这些转换,在进入下一个[状态]之前执行所有相关的系统。

状态栈

除了从一个状态完全转换到另一个状态,还可以覆盖状态,形成堆栈。

状态栈能实现“游戏暂停”屏幕或覆盖菜单等功能,而游戏世界仍在后台可见/运行。

可以让某些系统在状态“不活动”时(也就是在后台,其他状态在上面运行)仍在运行。也可以添加一次性系统,在“暂停”或“恢复”状态时运行。[TODO:需要实践一下“一次性系统”]

在 [App Builder] 中:

#![allow(unused)]
fn main() {
    // 一直运行
    .add_system_set(
        SystemSet::on_update(AppState::InGame)
            .with_system(playe_movement)
    )

    // TODO:player idle animation while paused
    .add_system_set(
        SystemSet::on_inactive_update(AppState::InGame)
            .with_system(player_idle)
    )

    // TODO:animations both while paused and while active
    .add_system_set(
        SystemSet::on_in_stack_update(AppState::InGame)
            .with_system(animate_trees)
            .with_system(animate_water)
    )

    // 在这个状态“暂停”时运行
    .add_system_set(
        SystemSet::on_pause(AppState::InGame)
            .with_system(hide_enemies)
    )

    // 在这个状态“恢复”时运行
    .add_system_set(
        SystemSet::on_resume(AppState::InGame)
            .with_system(reset_player)
    )

    // 在这个状态第一次“进入”时运行
    .add_system_set(
        SystemSet::on_enter(AppState::InGame)
            .with_system(setup_player)
            .with_system(setup_map)
    )

    // 在这个状态“离开”时运行
    .add_system_set(
        SystemSet::on_exit(AppState::InGame)
            .with_system(despawn_player)
            .with_system(despawn_map)
    )
}

pushpop 来管理状态:

#![allow(unused)]
fn main() {
    // 推入 Paused 状态
    app_state.push(AppState::Paused).unwrap();
    // 弹出当前状态(Paused 状态),回到之前的状态
    app_state.pop().unwrap();
}

(如前所示,用 .set 替换栈顶的活动状态)

已知的陷阱和限制

与其他运行标准结合

因为 State 是用 RunCriteria 来实现的,所以不能与 RunCriteria(例如用到 [Fixed Timestep])结合使用。

如果在使用了 State 的 SystemSet 上添加一个 RunCriteria,就会取代 State,表现为 State 无效,RunCriteria 生效。

使用一些技巧仍然可以做到把状态和运行标准结合起来使用。(TODO) show an example of how it could be done.

可以考虑 iyes_loopless,它没有此类限制。

Multiple Stages(TODO)

If you need state-dependent systems in multiple stages, a workaround is required.

You must add the state to one stage only, and then call .get_driver() and add that to the other stages before any state-dependent system sets in those stages.

可以考虑 iyes_loopless,它没有此类限制。

附带输入

如果要用 Input<T>(按钮/键盘)触发状态转换,要调用 .reset手动清除输入:

#![allow(unused)]
fn main() {
fn esc_to_menu(
    mut keys: ResMut<Input<KeyCode>>,
    mut app_state: ResMut<State<AppState>>,
) {
    if key.just_pressed(KeyCode::Escape) {
        app_state.set(AppState::MainMenu).unwrap();
        keys.reset(KeyCode::Escape);
    }
}
}

(注意,要求使用 ResMut

不用 .reset 会引起问题(目前是 issue),查看 triggering state transition from player input locks the game

iyes_loopless没有此 issue。

事件

如果某个系统不是每帧运行,那么在暂停状态期间,系统会错过所有事件。避免这个陷阱的方法是实现 [Manual Event Clearing],手动管理相关事件类型的生命周期。