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
每月下载量 1,187
在 2 crates 中使用
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