#bevy #状态转换 #阶段 #游戏开发 #固定 #系统 #运行

iyes_loopless

Bevy 的 States/FixedTimestep/RunCriteria 的可组合替代方案

16 个版本 (9 个重大更改)

0.9.1 2022 年 11 月 20 日
0.8.0 2022 年 10 月 24 日
0.7.0 2022 年 7 月 31 日
0.1.1 2022 年 3 月 23 日

游戏开发 中排名第 664

Download history 354/week @ 2024-03-13 386/week @ 2024-03-20 366/week @ 2024-03-27 365/week @ 2024-04-03 296/week @ 2024-04-10 379/week @ 2024-04-17 360/week @ 2024-04-24 430/week @ 2024-05-01 392/week @ 2024-05-08 372/week @ 2024-05-15 375/week @ 2024-05-22 351/week @ 2024-05-29 298/week @ 2024-06-05 279/week @ 2024-06-12 318/week @ 2024-06-19 244/week @ 2024-06-26

每月下载量 1,187
2 crates 中使用

MIT/Apache

99KB
1.5K SLoC

Bevy 的 RunCriteria、States、FixedTimestep 的可组合替代方案

该软件包提供了 Bevy 游戏引擎当前提供的 Run Criteria、States 和 FixedTimestep 调度功能的替代方案。

该软件包提供的解决方案不使用 "循环阶段",因此可以优雅地组合在一起,解决了 Bevy 中相应 API 的一些最令人烦恼的可用性限制。

版本兼容性表

Bevy 版本 软件包版本
主要 bevy_main
0.9 主要
0.9 0.9
0.8 0.7, 0.8
0.7 0.4, 0.5, 0.6
0.6 0.1, 0.2, 0.3

这与 Bevy 无阶段 RFC 有何关系?

该软件包受到了 Bevy 的 "无阶段 RFC" 建议的极大启发。

向所有参与该 RFC 和其中描述的设计的作者表示衷心的感谢。

我制作这个软件包,因为我相信 Bevy 中当前的 API 急需进行可用性改进。

我想出了一个方法,可以在现有 Bevy 框架内实现 Stageless RFC 中的想法,而不需要像 RFC 建议的那样彻底重写调度 API。

这样我们就可以现在就有可用性,同时剩余的无阶段工作仍在进行中。

依赖关系和 Cargo 功能标志

"运行条件" 功能始终启用,仅依赖于 bevy_ecs

"固定时间步长" 功能是可选的("fixedtimestep" cargo 功能)并添加以下依赖项

  • bevy_time
  • bevy_utils

"状态" 功能是可选的("states" cargo 功能)并添加以下依赖项

  • bevy_utils

"app" cargo 功能启用扩展特性,这些特性向 App 添加新的构建方法,允许更方便地访问该软件包的功能。添加了对 bevy_app 的依赖。

“bevy-compat”功能增加了运行条件,以与Bevy的遗留状态实现保持兼容。

默认情况下,所有可选的Cargo功能都已被启用。

运行条件

此包提供了一种名为“运行条件”的替代方案,用于Bevy的运行标准。

选择不同的名称是为了避免与Bevy中的API发生命名冲突和混淆。Bevy运行标准已经深度集成到Bevy的调度模型中,而此包不会接触/替换它们。它们在技术上仍然存在并且可使用。

运行条件是如何工作的?

您可以将任何Bevy系统转换为“条件系统”。这允许您通过重复调用.run_if构建器方法来添加任意数量的“条件”。

每个条件都是一个输出(返回)布尔值的Bevy系统。

条件系统将自身呈现给Bevy作为一个单独的大系统(类似于Bevy的系统管道),结合了创建它的系统以及附加的所有条件系统。

运行时,它将运行每个条件,如果其中任何一个返回false,则终止。只有当所有条件返回true时,主系统才会运行。

(请参阅examples/conditions.rs以获取更完整的示例)

use bevy::prelude::*;
use iyes_loopless::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_system(
            notify_server
                .run_if(in_multiplayer)
                .run_if(on_mytimer)
        )
        .run();
}

/// Condition checking our timer
fn on_mytimer(mytimer: Res<MyTimer>) -> bool {
    mytimer.timer.just_finished()
}

/// Condition checking if we are connected to multiplayer server
fn in_multiplayer(gamemode: Res<GameMode>, connected: Res<ServerState>) -> bool {
    *gamemode == GameMode::Multiplayer &&
    connected.is_active()
}

/// Some system that should only run on a timer in multiplayer
fn notify_server(/* ... */) {
    // ...
}

强烈建议所有条件系统仅以不可变方式访问数据。避免在条件系统中进行可变访问或使用局部变量,除非您非常确定自己在做什么。如果您将相同的条件添加到多个系统中,它将针对每个系统运行

还有一些辅助方法,可以轻松添加常见类型的运行条件

  • .run_if_not:反转条件的输出
  • .run_on_event::<T>():如果有特定类型的事件,则运行
  • .run_if_resource_exists::<T>():如果存在特定类型的资源,则运行
  • .run_unless_resource_exists::<T>():如果不存在特定类型的资源,则运行
  • .run_if_resource_equals(value):如果资源的值等于提供的值,则运行
  • .run_unless_resource_equals(value):如果资源的值不等于提供的值,则运行

如果您正在使用状态

  • .run_in_state(状态)
  • .run_not_in_state(状态)

如果您需要使用经典Bevy状态,可以使用这些适配器使用运行条件来检查它们

  • .run_in_bevy_state(状态)
  • .run_not_in_bevy_state(状态)

您可以使用Bevy标签进行系统排序,就像通常一样。

注意:条件系统目前仅支持显式标签,您不能使用Bevy的“按函数名称排序”语法。例如:.after(another_system)不会工作,您需要创建一个标签。

此外还有 ConditionSet(类似于 Bevy 的 SystemSet):用于轻松应用许多系统共有的条件和标签的语法糖。

use bevy::prelude::*;
use iyes_loopless::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_system(
            notify_server
                .run_if(in_multiplayer)
                .run_if(on_mytimer)
                // use bevy labels for ordering, as usual :)
                // (must be added at the end, after the conditions)
                .label("thing")
                .before("thing2")
        )
        // You can easily apply many conditions to many systems
        // using a `ConditionSet`:
        .add_system_set(ConditionSet::new()
            // all the conditions, and any labels/ordering
            // must be added before adding the systems
            // (helps avoid confusion and accidents)
            // (makes it clear they apply to all systems in the set)
            .run_if(in_multiplayer)
            .run_if(other_condition)
            .label("thing2")
            .after("stuff")
            .with_system(system1)
            .with_system(system2)
            .with_system(system3)
            .into() // Converts into Bevy `SystemSet` (to add to App)
        )
        .run();
}

注意:由于 Bevy 的一些限制,label/before/after 不支持在 ConditionSet 中的单个系统中使用。您只能在整个集合上使用标签和排序,以将其应用到所有成员系统。如果某些系统需要不同的排序,只需使用 .add_system 单独添加即可。

固定时间步长

此软件包提供了一个固定时间步长的实现,它在 Bevy 调度中作为一个独立的阶段运行。这样,它不会与其他任何功能冲突。您可以轻松使用 运行条件状态 来控制您的固定时间步长系统。

在固定时间步长内可以添加多个“子阶段”,允许您在一个时间步长运行中应用 Commands。例如,如果您想在同一tick上创建实体并对它们进行操作,可以使用这种方法。

如果需要,还可以有多个独立的固定时间步长。

(有关更复杂的示例,请参阅 examples/fixedtimestep.rs

use bevy::prelude::*;
use iyes_loopless::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // add the fixed timestep stage:
        // (in the default position, before CoreStage::Update)
        .add_fixed_timestep(
            Duration::from_millis(250),
            // we need to give it a string name, to refer to it
            "my_fixed_update",
        )
        // add fixed timestep systems:
        .add_fixed_timestep_system(
            "my_fixed_update", 0, // fixed timestep name, sub-stage index
            // it can be a conditional system!
            my_simulation
                .run_if(some_condition)
                .run_in_state(AppState::InGame)
                .after("some_label")
        )
        .run();
}

在每一帧,FixedTimestepStage 将累积时间差。当它超过设置的时间步长值时,它将运行所有子阶段。如果累积了多个时间步长,它将重复执行子阶段序列多次。

固定时间步长控制

您可以使用 FixedTimesteps 资源(确保它是来自本软件包的,而不是来自 Bevy 的同名资源)来访问有关固定时间步长的信息并控制其参数,如时间步长持续时间。

fn timestep_control(mut timesteps: ResMut<FixedTimestep>) {
    // we can access our timestep by name
    let info = timesteps.get_mut("my_fixed_update").unwrap();
    // set a different duration
    info.step = Duration::from_millis(125);
    // pause it
    info.paused = true;
}

/// Print info about the fixed timestep this system runs in
fn debug_fixed(timesteps: Res<FixedTimesteps>) {
    // from within a system that runs inside the fixed timestep,
    // you can use `.get_current`, no need for the timestep name:
    let info = timesteps.get_current().unwrap();
    println!("Fixed timestep duration: {:?} ({} Hz).", info.timestep(), info.rate());
    println!("Overstepped by {:?} ({}%).", info.remaining(), info.overstep() * 100.0);
}

状态

(有关完整示例,请参阅 examples/menu.rs

此软件包提供了一个状态抽象,其工作方式如下:

您创建一个(或多个)状态类型,通常枚举,就像使用 Bevy 状态一样。

然而,这里我们使用两种资源类型来跟踪状态:

  • CurrentState(T):您当前所处的状态
  • NextState(T):每当您想更改状态时,插入此(使用 Commands

注册状态类型

您需要使用 .add_loopless_state(value) 将状态添加到您的 App 中,并提供初始状态值。此辅助方法添加了一个特殊阶段类型(StateTransitionStage),负责执行状态转换。默认情况下,它添加在 CoreStage::Update 之前。如果您希望转换在应用程序调度的其他位置执行,还有其他辅助方法可以指定位置。

对于高级用例,您可以手动构造并添加 StateTransitionStage,而不使用辅助方法。

进入/退出系统

您可以使用 .add_enter_system(state, system).add_exit_system(state, system) 添加进入/退出系统,以便在状态转换时执行。

对于高级场景,您可以使用自定义阶段类型,通过使用.set_enter_stage(state, stage).set_exit_stage(state, stage)来实现。

状态转换

StateTransitionStage运行时,它会检查是否存在NextState资源。如果存在,它将删除该资源并执行转换。

  • 运行当前状态下的“退出阶段”(如果有的话)
  • 更改CurrentState的值
  • 为下一个状态运行“进入阶段”(如果有的话)

如果您想执行状态转换,只需插入一个NextState<T>。如果您修改CurrentState<T>,您将实际更改状态而不会运行退出/进入系统(您可能不想这样做)。

如果在一个退出/进入阶段内部插入一个新的NextState实例,可以在单个帧中执行多个状态转换。

更新系统

对于您想要每帧运行的系统,我们提供了.run_in_state(state).run_not_in_state(state) 运行条件

您可以在任何地方添加系统,到任何阶段(包括在固定时间步长之后),并使用这些辅助方法使它们依赖于一个或多个状态。

use bevy::prelude::*;
use iyes_loopless::prelude::*;

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

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // Add our state type
        .add_loopless_state(GameState::MainMenu)
        // If we had more state types, we would add them too...

        // Add a FixedTimestep, cuz we can!
        .add_fixed_timestep(
            Duration::from_millis(250),
            "my_fixed_update",
        )
        .add_fixed_timestep_system(
            "my_fixed_update", 0,
            my_simulation
                .run_in_state(AppState::InGame)
        )

        // Add our various systems
        .add_system(menu_stuff.run_in_state(GameState::MainMenu))
        .add_system(animate.run_in_state(GameState::InGame))

        // On states Enter and Exit
        .add_enter_system(GameState::MainMenu, setup_menu)
        .add_exit_system(GameState::MainMenu, despawn_menu)
        .add_enter_system(GameState::InGame, setup_game)

        .run();
}

固定时间步长的状态转换

如果您有一个用于控制固定时间步长内容的州类型,您可能希望只有固定时间步长(而不是任何帧)发生状态转换。

为了实现这一点,您可以在您的FixedTimestepStage的开始处添加StateTransitionStage作为子阶段。

该存储库中的阶段类型可以像那样组合! :) 它们接受任何阶段类型。

依赖关系

~7–26MB
~364K SLoC