状态

可参考的官方例子: state.


状态允许你构造你应用程序的运行时"流程"。

下面是你可以通过状态实现的功能,比如:

  • 一个菜单页或一个加载页
  • 暂停或结束暂停游戏
  • 不同的游戏模式
  • ...

在每个状态下,你可以有不同的系统被执行。你也可以添加一次性的设置和清理系统,以更在进入或退出一个状态时运行。

要使用状态,请定义一个枚举类型,并在你的应用程序构造器中添加系统集

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

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

            // add the app state type
            .add_state(AppState::MainMenu)

            // add systems to run regardless of state, as usual
            .add_system(play_music)

            // systems to run only in the main menu
            .add_system_set(
                SystemSet::on_update(AppState::MainMenu)
                    .with_system(handle_ui_buttons)
            )

            // setup when entering the state
            .add_system_set(
                SystemSet::on_enter(AppState::MainMenu)
                    .with_system(setup_menu)
            )

            // cleanup when exiting the state
            .add_system_set(
                SystemSet::on_exit(AppState::MainMenu)
                    .with_system(close_menu)
            )
            .run();
    }

对多个系统集使用相同的状态是可以的。

当你想设置标签和使用明确的系统排序时,这非常有用。

这对插件来说也很有用。每个插件都可以为同一个状态添加自己的系统集。

状态在底层是通过执行条件实现的。这些特殊的系统集构造函数实际上只是为自动添加状态管理执行条件的辅助工具。

控制状态

在系统的内部,你可以使用 State<T> 资源检查和控制状态。

    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
            }
        }
    }

要改变到另一个状态:

    fn enter_game(mut app_state: ResMut<State<AppState>>) {
        app_state.set(AppState::InGame).unwrap();
        // ^ this can fail if we are already in the target state
        // or if another state change is already queued
    }

在当前状态的系统执行完成后,Bevy 将会转换到你设定的下一个状态。

你可以在一帧更新中做任意多的状态转换。Bevy 会处理好所有这些状态转换,并执行所有相关的系统(在进入下一阶段之前)。

状态栈

你不需要完全从一个状态转换到另一个状态,你也可以进行叠加状态,形成一个状态堆栈。

这就是你如何实现诸如"游戏暂停"屏幕、或一个菜单叠加屏幕的同时,而游戏世界仍然可见或在后景运行。

你可以有一些系统,即使在所属状态"未激活"时仍然在运行(也就是该状态在后台,其他状态在它上面运行)。你也可以在"暂停"或"恢复"状态时来添加并运行一次性系统。

在你的应用程序构造器中如下:

            // player movement only when actively playing
            .add_system_set(
                SystemSet::on_update(AppState::InGame)
                    .with_system(player_movement)
            )
            // player idle animation while paused
            .add_system_set(
                SystemSet::on_inactive_update(AppState::InGame)
                    .with_system(player_idle)
            )
            // 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)
            )
            // things to do when becoming inactive
            .add_system_set(
                SystemSet::on_pause(AppState::InGame)
                    .with_system(hide_enemies)
            )
            // things to do when becoming active again
            .add_system_set(
                SystemSet::on_resume(AppState::InGame)
                    .with_system(reset_player)
            )
            // setup when first entering the game
            .add_system_set(
                SystemSet::on_enter(AppState::InGame)
                    .with_system(setup_player)
                    .with_system(setup_map)
            )
            // cleanup when finally exiting the game
            .add_system_set(
                SystemSet::on_exit(AppState::InGame)
                    .with_system(despawn_player)
                    .with_system(despawn_map)
            )

为了管理这样的状态栈,使用 push/pop

        // to go into the pause screen
        app_state.push(AppState::Paused).unwrap();
        // to go back into the game
        app_state.pop().unwrap();

(如前所示,使用 .set 方法来替换堆栈顶部的活动状态)

已知的陷阱和限制

事件

当在不是一直运行的系统中接收事件时,例如在暂停状态下,你会错过任何在接收系统不运行的帧中发送的事件

为了减轻这种情况,你可以实现一个自定义的清理策略,来手动管理相关事件类型的生命周期。

结合其它执行条件

因为状态是用执行条件来实现的,所以把它们和其他使用的执行条件结合起来将会很棘手,比如固定的时间步长。

如果你试图给你的系统集添加另一个执行条件,它将取代 Bevy 的状态管理执行条件!这将使系统集的执行不再受到状态的约束。

使用一些技巧仍然有可能完成这样的目的。

(TODO)展示一个例子,说明如何做到这一点。

With Input

如果你想通过 Input<T> 的按键被按下来触发状态转换的话,随即你需要通过调用 .reset 手动清除刚才的输入:

    fn esc_to_menu(
        mut keys: ResMut<Input<KeyCode>>,
        mut app_state: ResMut<State<AppState>>,
    ) {
        if keys.just_pressed(KeyCode::Escape) {
            app_state.set(AppState::MainMenu).unwrap();
            keys.reset(KeyCode::Escape);
        }
    }

(注意,这需要 ResMut)

不这样做会导致出现这个问题

多阶段

如果你需要在多个阶段中依赖状态系统,这需要一个变通的方法。

你必须只将状态添加到一个阶段,然后调用 .get_driver() 并在这些阶段中的任何依赖状态的系统集之前将其添加到其他阶段。

(TODO)例子