Bevy 0.5

发表于 2021 年 4 月 6 日,作者:Carter Anderson ( 一只戴着猫耳朵挥舞着触手的黑影,即 Octocat:GitHub 的吉祥物和 logo @cart 一只灰色鸟飞翔的矢量图;X(前身为 Twitter)的旧 logo @cart_cart 一个指向右边的三角形在圆角矩形中;YouTube 的 logo cartdev )

Screenshot of Ante: a voxel builder game being developed in Bevy by @TheNeikos
Ante 的屏幕截图:一款由 @TheNeikos 使用 Bevy 开发的体素建造游戏

感谢 **88** 位贡献者、**283** 个拉取请求以及我们的 慷慨的赞助商,我很高兴地宣布 **Bevy 0.5** 发布在 crates.io 上!

对于那些不了解 Bevy 的人来说,Bevy 是一个用 Rust 编写的令人耳目一新的简单数据驱动游戏引擎。您可以查看 快速入门指南 来开始使用。Bevy 也是永远免费和开源的!您可以在 GitHub 上获取完整的 源代码。查看 Awesome Bevy 以了解社区开发的插件、游戏和学习资源列表。

**Bevy 0.5** 比我们过去几次发布的版本要大得多(而且花了更长的时间),因为我们进行了一些基础性更改。如果您计划将您的 App 或插件更新到 **Bevy 0.5**,请查看我们的 0.4 到 0.5 迁移指南

以下是本次发布的亮点

基于物理的渲染 (PBR) #

作者:@StarArawn、@mtsr、@mockersf、@IngmarBitter、@Josh015、@norgate、@cart

Bevy 现在在渲染时使用 PBR 着色器。PBR 是一种半标准的渲染方法,它试图使用对真实世界“基于物理”的照明和材质属性的近似值。我们主要使用来自 Filament PBR 实现的技术,但我们也加入了一些来自 虚幻引擎迪士尼 的想法。

Bevy 的 StandardMaterial 现在具有 base_colorroughnessmetallicreflectionemissive 属性。它现在还支持 base_colornormal_mapmetallic_roughnessemissiveocclusion 属性的纹理。

新的 PBR 示例有助于可视化这些新的材质属性

pbr

GLTF 改进 #

PBR 纹理 #

作者:@mtsr、@mockersf

GLTF 加载器现在支持法线贴图、金属/粗糙度、遮挡和自发光纹理。我们的“飞行头盔”gltf 示例利用了新的 PBR 纹理支持,因此看起来更漂亮了

顶级 GLTF 资产 #

作者:@mockersf

以前很难与 GLTF 资产交互,因为场景/网格/纹理和材质仅作为“子资产”加载。由于新的顶级 Gltf 资产类型,现在可以浏览 GLTF 资产的内容了

// load GLTF asset on startup
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
    let handle = assets.load("flight_helmet.gltf");
    commands.insert_resource(handle);
}

// access GLTF asset at some later point in time
fn system(handle: Res<Handle<Gltf>>, gltfs: Res<Assets<Gltf>>, materials: Res<Assets<StandardMaterial>>) {
    let gltf = gltfs.get(&handle).unwrap();
    let material_handle = gltf.named_materials.get("MetalPartsMat").unwrap();
    let material = materials.get(material_handle).unwrap();
}

Bevy ECS V2 #

本次发布标志着 Bevy 的 ECS 迈出了巨大的一步。它对 Bevy App 的组成方式及其性能有重大影响

继续阅读以了解详细信息!

ECS 核心重写 #

作者:@cart

到目前为止,Bevy 为我们的 ECS 核心使用了 hecs 的一个严重分叉版本。自 Bevy 首次发布以来,我们已经了解了很多关于 Bevy ECS 的需求。我们还与其他 ECS 项目负责人进行了合作,例如 Sander Mertensflecs 主要开发人员)和 Gijs-Jan Roelofs(Xenonauts ECS 框架开发人员)。作为“ECS 社区”,我们已经开始专注于 ECS 的未来。

Bevy ECS v2 是我们迈向未来迈出的第一步。这也意味着 Bevy ECS 不再是“hecs 分支”。我们正在自己走出去!

组件存储(问题) #

多年来,两种 ECS 存储范式获得了广泛的关注

  • 原型 ECS:
    • 将组件存储在具有静态模式的“表”中。每列存储特定类型的组件。每行都是一个实体。
    • 每个“原型”都有自己的表。添加/删除实体的组件会更改原型。
    • 由于其缓存友好的数据布局,因此可以实现超快的查询迭代
    • 以实体组件的添加/删除操作更昂贵为代价,因为所有组件都需要复制到新的原型“表”中
    • 并行友好:实体一次只存在于一个原型中,因此访问相同组件但位于不同原型的系统可以并行运行
    • 框架:旧的 Bevy ECS、hecs、legion、flecs、Unity DOTS
  • 稀疏集 ECS:
    • 将相同类型的组件存储在密集打包的数组中,这些数组由密集打包的无符号整数(实体 ID)稀疏索引
    • 查询迭代比原型 ECS 慢(默认情况下),因为每个实体的组件都可能位于稀疏集中的任意位置。这种“随机访问”模式不是缓存友好的。此外,存在一层额外的间接层,因为您必须首先将实体 ID 映射到组件数组中的索引。
    • 添加/删除组件是一个便宜的、恒定时间操作
    • “组件包”用于优化逐案迭代性能(但包相互冲突)
    • 并行性不友好:系统需要要么锁定整个组件存储(非粒度),要么锁定单个实体(昂贵)
    • 框架:Shipyard、EnTT

选择 ECS 框架的开发人员面临着艰难的选择。选择一个具有“快速迭代无处不在”的“原型”框架,但没有廉价添加/删除组件的能力,或者选择一个具有廉价添加/删除组件但迭代性能较慢或手动(且冲突)包优化的“稀疏集”框架。

混合组件存储(解决方案) #

在 Bevy ECS V2 中,我们既能鱼与熊掌兼得。它现在同时具有上述两种组件存储类型(如果需要,以后可以添加更多)

  • **表**(也称为其他框架中的“原型”存储)
    • 默认存储。如果您没有配置任何内容,这就是您得到的
    • 默认情况下快速迭代
    • 添加/删除操作较慢
  • 稀疏集
    • 选择加入
    • 迭代较慢
    • 添加/删除操作更快

这些存储类型完美地相互补充。默认情况下,查询迭代速度很快。如果开发人员知道他们想以高频率添加/删除组件,他们可以将存储设置为“稀疏集”

app.register_component(
    ComponentDescriptor::new::<MyComponent>(StorageType::SparseSet)
);

组件添加/删除基准测试(以毫秒为单位,越小越好) #

该基准测试说明了从具有 5 个其他 4x4 矩阵组件的实体中添加和删除单个 4x4 矩阵组件 10,000 次。包含“其他”组件是为了帮助说明“表存储”的成本(由 Bevy 0.4、Bevy 0.5(表)和 Legion 使用),这需要将“其他”组件移动到新表中。

component add/remove

您可能已经注意到,**Bevy 0.5(表)** 也比 **Bevy 0.4** 快得多,即使它们都使用“表存储”。这主要是由于新的 原型图,它显著降低了原型更改的成本。

有状态查询和系统参数 #

World 查询(以及其他系统参数)现在是有状态的。这使我们能够

  1. 缓存原型(和表)匹配
    • 这解决了(朴素)原型 ECS 的另一个问题:随着原型数量的增加(和碎片的出现),查询性能会下降。
  2. 缓存查询获取和筛选状态
    • 获取/筛选操作的昂贵部分(例如对 TypeId 进行哈希以找到 ComponentId)现在只在查询首次构建时发生一次
  3. 逐步构建状态
    • 当添加新的原型时,我们只处理新的原型(无需为旧原型重建状态)

因此,直接的 World 查询 API 现在看起来像这样

let mut query = world.query::<(&A, &mut B)>();
for (a, mut b) in query.iter_mut(&mut world) {
}

但是对于系统而言,这是一个非破坏性更改。查询状态管理由相关的 SystemParam 在内部完成。

新版 [Query] 系统为我们带来了显著的性能提升。

稀疏碎片化迭代器基准测试(纳秒,越小越好) #

此基准测试运行一个查询,该查询匹配单个原型中的 5 个实体,并且匹配其他 100 个原型。这是对游戏中“现实世界”查询的合理测试,这些查询通常具有许多不同的实体“类型”,其中大多数匹配给定的查询。此测试在整个过程中使用“表格存储”。

sparse_frag_iter

Bevy 0.5 由于新的“有状态查询”带来了巨大的改进。Bevy 0.4 每次运行迭代器时都需要检查每个原型,而Bevy 0.5 将该成本摊销为零。

碎片化迭代器基准测试(毫秒,越小越好) #

这是 ecs_bench_suitefrag_iter 基准测试。它对 27 个原型(每个原型包含 20 个实体)运行查询。但是,与“稀疏碎片化迭代器基准测试”不同,没有“不匹配”的原型。此测试在整个过程中使用“表格存储”。

frag_iter

与上一个基准测试相比,这里的收益较小,因为没有不匹配的原型。但是,由于更好的迭代器/查询实现,Bevy 0.5 仍然获得了不错的提升,将匹配原型的成本摊销为零,并且使用了 for_each 迭代器。

超级快的“for_each”查询迭代器 #

开发人员现在可以选择使用快速 Query::for_each 迭代器,这对于“碎片化迭代”可以提高迭代速度约 1.5-3 倍,对于非碎片化迭代可以提高迭代速度约 1.2 倍。

fn system(query: Query<(&A, &mut B)>) {
    // you now have the option to do this for a speed boost
    query.for_each_mut(|(a, mut b)| {
    });

    // however normal iterators are still available
    for (a, mut b) in query.iter_mut() {
    }
}

我们将继续鼓励使用“普通”迭代器,因为它们更加灵活,更符合 Rust 惯例。但是,当需要额外的“动力”时,for_each 会在那里……等待着你 :)

新的并行系统执行器 #

作者:@Ratysz

Bevy 的旧并行执行器存在一些基本限制

  1. 明确定义系统顺序的唯一方法是创建新的阶段。这既繁琐又会阻止并行化(因为阶段按顺序“逐个”运行)。我们注意到系统排序是常见的要求,而阶段无法满足这种需求。
  2. 当系统访问冲突资源时,它们会具有“隐式”排序。这些排序很难理解。
  3. “隐式排序”会生成执行策略,这些策略通常会浪费很多并行化的潜力。

幸运的是,@Ratysz 一直在这个领域进行 大量 研究,并自愿贡献一个新的执行器。新的执行器解决了上面提到的所有问题,还添加了许多新的可用性改进。“排序”规则现在非常简单

  1. 系统默认情况下并行运行
  2. 具有明确定义的排序的系统将遵守这些排序

明确的系统依赖关系和系统标签 #

作者:@Ratysz, @TheRawMeatball

系统现在可以分配一个或多个 SystemLabels。这些标签可以被其他系统(在一个阶段内)引用,以在具有该标签的系统之前或之后运行

app
    .add_system(update_velocity.system().label("velocity"))
    // The "movement" system will run after "update_velocity" 
    .add_system(movement.system().after("velocity"))

这会产生等效的排序,但它使用的是 before() 而不是 after()

app
    // The "update_velocity" system will run before "movement" 
    .add_system(update_velocity.system().before("movement"))
    .add_system(movement.system().label("movement"));

任何实现 SystemLabel 特征的类型都可以使用。在大多数情况下,我们建议定义自定义类型并为它们派生 SystemLabel。这可以防止拼写错误,允许封装(在需要时),并允许 IDE 自动完成标签

#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub enum PhysicsSystem {
    UpdateVelocity,
    Movement,
}

app
    .add_system(update_velocity.system().label(PhysicsSystem::UpdateVelocity))
    .add_system(movement.system()
        .label(PhysicsSystem::Movement)
        .after(PhysicsSystem::UpdateVelocity)
    );

多对多系统标签 #

多对多标签是一个强大的概念,它使依赖于生成给定行为/结果的多个系统变得容易。例如,如果你有一个系统需要在所有“物理”完成更新后运行(参见上面的示例),你可以用相同的 Physics 标签标记所有“物理系统”

#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub struct Physics;

#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub enum PhysicsSystem {
    UpdateVelocity,
    Movement,
}

app
    .add_system(update_velocity.system()
        .label(PhysicsSystem::UpdateVelocity)
        .label(Physics)
    )
    .add_system(movement.system()
        .label(PhysicsSystem::Movement)
        .label(Physics)
        .after(PhysicsSystem::UpdateVelocity)
    )
    .add_system(runs_after_physics.system().after(Physics));

Bevy 插件作者应该在其公共 API 中导出此类标签,以使他们的用户能够在插件提供的逻辑之前/之后插入系统。

系统集 #

SystemSets 是一种新的方法,可以将相同的配置应用于一组系统,这大大减少了样板代码。上面的“物理”示例可以这样改写

app
    .add_system_set(SystemSet::new()
        // this label is added to all systems in the set
        .label(Physics)
        .with_system(update_velocity.system().label(PhysicsSystem::UpdateVelocity))
        .with_system(movement.system()
            .label(PhysicsSystem::Movement)
            .after(PhysicsSystem::UpdateVelocity)
        )
    )

SystemSets 也可以使用 before(Label)after(Label) 来在给定标签之前/之后运行集合中的所有系统。

这对于需要使用相同 RunCriteria 运行的一组系统也非常有用。

app
    // all systems in this set will run once every two seconds
    .add_system_set(SystemSet::new()
        .with_run_criteria(FixedTimestep::step(2.0))
        .with_system(foo.system())
        .with_system(bar.system())
    )

改进的运行条件 #

运行条件现在与系统分离,并在可能的情况下重复使用。例如,上面的示例中的 FixedTimestep 条件将只在每个阶段运行一次。执行器将对 foobar 系统重复使用条件的结果。

运行条件现在也可以被标记并被其他系统引用

fn every_other_time(mut has_ran: Local<bool>) -> ShouldRun {
    *has_ran = !*has_ran;
    if *has_ran {
        ShouldRun::Yes
    } else {
        ShouldRun::No
    }
}

app.add_stage(SystemStage::parallel()
   .with_system_run_criteria(every_other_time.system().label("every_other_time")))
   .add_system(foo.system().with_run_criteria("every_other_time"))

来自运行条件的结果也可以被“管道”输送到其他条件中,这可以实现有趣的组合行为

fn once_in_a_blue_moon(In(input): In<ShouldRun>, moon: Res<Moon>) -> ShouldRun {
    if moon.is_blue() {
        input
    } else {
        ShouldRun::No
    }
}

app
    .add_system(foo.with_run_criteria(
        "every_other_time".pipe(once_in_a_blue_moon.system())
    )

歧义检测和解决 #

虽然新的执行器现在更容易理解,但它确实引入了一类新的错误:“系统排序歧义”。当两个系统与相同的数据交互,但没有定义明确的排序时,它们产生的输出是非确定性的(并且通常不是作者的意图)。

考虑以下应用程序

fn increment_counter(mut counter: ResMut<usize>) {
    *counter += 1;
}

fn print_every_other_time(counter: Res<usize>) {
    if *counter % 2 == 0 {
        println!("ran");
    }
}

app
    .add_system(increment_counter.system())
    .add_system(print_every_other_time.system())

作者显然希望 print_every_other_time 每隔一次更新运行一次。但是,由于这些系统没有定义顺序,它们可能在每次更新时以不同的顺序运行,从而导致在两次更新的过程中没有任何内容被打印出来

UPDATE
- increment_counter (counter now equals 1)
- print_every_other_time (nothing printed)
UPDATE
- print_every_other_time (nothing printed)
- increment_counter (counter now equals 2)

旧的执行器会隐式地强制 increment_counter 首先运行,因为它与 print_every_other_time 冲突,并且它被首先插入。但新的执行器要求你在这里明确地定义顺序(我们认为这是一件好事)。

为了帮助检测这类错误,我们构建了一个可选的工具,可以检测这些歧义并将其记录下来

// add this resource to your App to enable ambiguity detection
app.insert_resource(ReportExecutionOrderAmbiguities)

然后,当我们运行我们的应用程序时,我们将在终端上看到以下消息

Execution order ambiguities detected, you might want to add an explicit dependency relation between some of these systems:
 * Parallel systems:
 -- "&app::increment_counter" and "&app::print_every_other_time"
    conflicts: ["usize"]

歧义检测器发现了一个冲突,并提到添加显式依赖关系可以解决冲突

app
    .add_system(increment_counter.system().label("increment"))
    .add_system(print_every_other_time.system().after("increment"))

确实存在一些歧义不是错误的情况,例如对 Assets 等无序集合的操作。这就是我们没有默认启用检测器的原因。你可以随意忽略这些歧义,但如果你想抑制检测器中的消息(而不定义依赖关系),可以将你的系统添加到“歧义集”中

app
    .add_system(a.system().in_ambiguity_set("foo"))
    .add_system(b.system().in_ambiguity_set("foo"))

我想强调的是,这完全是可选的。Bevy 代码应该是符合人体工程学的,并且编写起来“有趣”。如果你不习惯到处散布歧义集,那就不要担心!

我们也积极寻求对新的执行器的反馈。我们相信新的实现更容易理解,并鼓励编写自文档代码。改进的并行化也很不错!但是,我们想听听用户的意见(包括刚开始使用 Bevy 的新用户和将代码库移植到新执行器的旧用户)。这个领域都是关于设计权衡的,反馈将帮助我们确保我们做出了正确的决定。

可靠的更改检测 #

作者:@Davier, @bjorn3, @alice-i-cecile, @cart

全局更改检测(即在任何 ECS 组件或资源的 Changed/Added 状态上运行查询的能力)获得了重大的可用性提升:更改现在可以跨帧/更新检测到

// This is still the same change detection API we all know and love,
// the only difference is that it "just works" in every situation.
fn system(query: Query<Entity, Changed<A>>) {
    // iterates all entities whose A component has changed since
    // the last run of this system 
    for e in query.iter() {
    }
}

全局更改检测已经成为 Bevy 与其他 ECS 框架不同的一个特性,但现在它已经完全“万无一失”。它按预期工作,无论系统排序、阶段成员身份或系统运行条件如何。

旧的行为是“系统检测到本帧中之前运行的系统中发生的更改”。这是因为我们使用了一个 bool 来跟踪每个组件何时添加/修改。这个标志在每帧结束时会为每个组件清除。因此,用户必须非常小心操作顺序,使用“系统运行条件”之类的功能会导致更改丢失,如果系统在特定更新中没有运行的话。

我们现在使用了一个巧妙的“世界刻度”设计,允许系统检测到自上次运行以来的任何时间点发生的更改。

状态 V2 #

作者:@TheRawMeatball

上一次 Bevy 版本 添加了状态,使开发人员能够根据 State<T> 资源的值来运行 ECS 系统组。系统可以根据“状态生命周期事件”运行,例如 on_enter、on_update 和 on_exit。状态使在 Bevy ECS 中更容易对诸如单独的“加载屏幕”和“游戏内”逻辑进行编码。

旧的实现基本上是可行的,但它有一些怪癖和限制。首先,它需要添加一个新的 StateStage,这会减少并行化,增加样板代码,并强制进行不需要的排序。此外,一些生命周期事件的行为并不总是如预期的那样。

新的 State 实现建立在新的并行执行器的 SystemSet 和 RunCriteria 功能之上,提供了一个更自然、更灵活、更并行的 API,它基于现有的概念,而不是创建新的概念

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

fn main() {
    App::build()
        .add_state(AppState::Menu)
        .add_system_set(SystemSet::on_enter(AppState::Menu).with_system(setup_menu.system()))
        .add_system_set(SystemSet::on_update(AppState::Menu).with_system(menu_logic.system()))
        .add_system_set(SystemSet::on_exit(AppState::Menu).with_system(cleanup_menu.system()))
        .add_system_set(SystemSet::on_enter(AppState::InGame).with_system(setup_game.system()))
        .add_system_set(
            SystemSet::on_update(AppState::InGame)
                .with_system(game_logic.system())
                .with_system(more_game_logic.system())
        )
        .run();
}

状态现在使用“基于堆栈的状态机”模型。这为状态转换提供了许多选择

fn system(mut state: ResMut<State<AppState>>) {
    // Queues up a state change that pushes a new state on to the
    // stack (preserving previous states)
    state.push(AppState::InGame).unwrap();

    // Queues up a state change that removes the current state on
    // the stack and reverts to the previous state
    state.pop().unwrap();

    // Queues up a state change that overwrites the current state at
    // the "top" of the stack
    state.set(AppState::InGame).unwrap();

    // Queues up a state change that replaces the entire stack of states
    state.replace(AppState::InGame).unwrap();
}

与旧的实现一样,状态更改在同一帧中应用。这意味着可以从状态 A->B->C 进行转换,并运行相关状态生命周期事件,而不会跳过帧。这建立在“循环运行条件”之上,我们也将其用于我们的“固定时间步长”实现(你也可以将其用于你自己的运行条件逻辑)。

事件人体工程学 #

作者:@TheRawMeatball

事件现在具有用于更容易使用的首选简写语法

// Old Bevy 0.4 syntax
fn system(mut reader: Local<EventReader<SomeEvent>>, events: Res<Events<SomeEvent>>) {
    for event in reader.iter(&events) {
    }
}

// New Bevy 0.5 syntax
fn system(mut reader: EventReader<SomeEvent>) {
    for event in reader.iter() {
    }
}

现在还有一个对称的 EventWriter API

fn system(mut writer: EventWriter<SomeEvent>) {
    writer.send(SomeEvent { ... })
}

旧的“手动”方法仍然可以通过 ManualEventReader 使用

fn system(mut reader: Local<ManualEventReader<SomeEvent>>, events: Res<Events<SomeEvent>>) {
    for event in reader.iter(&events) {
    }
}

富文本 #

作者:@tigregalis

文本现在可以拥有“节”,每个节都有自己的样式/格式。这使得文本更加灵活,同时仍然遵守文本布局规则

rich_text

这是通过新的“文本节”API 实现的

commands
    .spawn_bundle(TextBundle {
        text: Text {
            sections: vec![
                TextSection {
                    value: "FPS: ".to_string(),
                    style: TextStyle {
                        font: asset_server.load("FiraSans-Bold.ttf"),
                        font_size: 90.0,
                        color: Color::WHITE,
                    },
                },
                TextSection {
                    value: "60.03".to_string(),
                    style: TextStyle {
                        font: asset_server.load("FiraMono-Medium.ttf"),
                        font_size: 90.0,
                        color: Color::GOLD,
                    },
                },
            ],
            ..Default::default()
        },
        ..Default::default()
    })

HIDPI 文本 #

作者:@blunted2night

文本现在根据当前显示器的缩放比例进行渲染。这在任何分辨率下都能提供清晰、锐利的文本。

hidpi_text

在 2D 世界空间中渲染文本 #

作者:@CleanCut, @blunted2night

文本现在可以使用新的 Text2dBundle 生成到 2D 场景中。这使得更容易实现诸如“在玩家上方绘制名称”之类的功能。

世界到屏幕坐标转换 #

作者:@aevyrie

现在可以使用新的 Camera::world_to_screen() 函数将世界坐标转换为给定摄像机的屏幕坐标。以下是如何使用此功能将 UI 元素放置在移动的 3D 对象顶部的示例。

3D 正投影相机 #

作者:@jamadazi

正投影相机现在可以在 3D 中使用!这对于诸如 CAD 应用程序和等距游戏之类的场景非常有用。

ortho_3d

正投影相机缩放模式 #

作者:@jamadazi

在 **Bevy 0.5** 之前,Bevy 的正交投影相机只有一种模式:“窗口缩放”。它会根据窗口的垂直和水平尺寸调整投影。这适用于某些类型的游戏,但其他游戏需要任意与窗口无关的缩放因子或由水平或垂直窗口尺寸定义的缩放因子。

**Bevy 0.5** 在 OrthographicCamera 中添加了一个新的 ScalingMode 选项,使开发者可以自定义投影的计算方式。

它还添加了使用 OrthographicProjection::scale 对相机进行“缩放”的功能。

灵活的相机绑定 #

作者:@cart

Bevy 以前为每个渲染图 PassNode“强行添加”相机绑定。当只有一个绑定类型(组合的 ViewProj 矩阵)时,这种方法有效,但许多着色器需要其他相机属性,例如世界空间位置。

在 Bevy 0.5 中,我们移除了这种“强行添加”方式,转而使用 RenderResourceBindings 系统,该系统在其他地方使用。这使得着色器可以绑定任意相机数据(使用任何集合或绑定索引),并且只获取它们需要的 data。

新的 PBR 着色器利用了此功能,但自定义着色器也可以使用它。

layout(set = 0, binding = 0) uniform CameraViewProj {
    mat4 ViewProj;
};
layout(set = 0, binding = 1) uniform CameraPosition {
    vec3 CameraPos;
};

渲染层 #

作者:@schell

有时你不希望相机绘制场景中的所有内容,或者你想要暂时隐藏场景中的一组内容。**Bevy 0.5** 添加了一个 RenderLayer 系统,使开发者可以通过添加 RenderLayers 组件,将实体添加到层级中。

相机也可以拥有一个 RenderLayers 组件,该组件决定它们可以看到哪些层级。

// spawn a sprite on layer 0
commands
    .spawn_bundle(SpriteBundle {
        material: materials.add(Color::rgb(1.0, 0.5, 0.5).into()),
        transform: Transform::from_xyz(0.0, -50.0, 1.0),
        sprite: Sprite::new(Vec2::new(30.0, 30.0)),
    })
    .insert(RenderLayers::layer(0));
// spawn a sprite on layer 1
commands
    .spawn_bundle(SpriteBundle {
        material: materials.add(Color::rgb(1.0, 0.5, 0.5).into()),
        transform: Transform::from_xyz(0.0, -50.0, 1.0),
        sprite: Sprite::new(Vec2::new(30.0, 30.0)),
    })
    .insert(RenderLayers::layer(1));
// spawn a camera that only draws the sprite on layer 1
commands
    .spawn_bundle(OrthographicCameraBundle::new_2d());
    .insert(RenderLayers::layer(1));

精灵翻转 #

作者:@zicklag

精灵现在可以轻松(且高效地)沿着 x 轴或 y 轴翻转。

sprite_flipping

commands.spawn_bundle(SpriteBundle {
    material: material.clone(),
    transform: Transform::from_xyz(150.0, 0.0, 0.0),
    ..Default::default()
});
commands.spawn_bundle(SpriteBundle {
    material,
    transform: Transform::from_xyz(-150.0, 0.0, 0.0),
    sprite: Sprite {
        // Flip the logo to the left
        flip_x: true,
        // And don't flip it upside-down ( the default )
        flip_y: false,
        ..Default::default()
    },
    ..Default::default()
});

色彩空间 #

作者:@mockersf

Color 现在在内部表示为枚举,这使得无损(且正确)的色彩表示成为可能。这比之前的实现有了显著的改进,之前的实现将所有颜色内部转换为线性 sRGB(这会导致精度问题)。颜色现在只在发送到 GPU 时才会转换为线性 sRGB。我们还借此机会修复了一些定义在错误色彩空间中的不正确的颜色常量。

pub enum Color {
    /// sRGBA color
    Rgba {
        /// Red component. [0.0, 1.0]
        red: f32,
        /// Green component. [0.0, 1.0]
        green: f32,
        /// Blue component. [0.0, 1.0]
        blue: f32,
        /// Alpha component. [0.0, 1.0]
        alpha: f32,
    },
    /// RGBA color in the Linear sRGB colorspace (often colloquially referred to as "linear", "RGB", or "linear RGB").
    RgbaLinear {
        /// Red component. [0.0, 1.0]
        red: f32,
        /// Green component. [0.0, 1.0]
        green: f32,
        /// Blue component. [0.0, 1.0]
        blue: f32,
        /// Alpha component. [0.0, 1.0]
        alpha: f32,
    },
    /// HSL (hue, saturation, lightness) color with an alpha channel
    Hsla {
        /// Hue component. [0.0, 360.0]
        hue: f32,
        /// Saturation component. [0.0, 1.0]
        saturation: f32,
        /// Lightness component. [0.0, 1.0]
        lightness: f32,
        /// Alpha component. [0.0, 1.0]
        alpha: f32,
    },
}

线框 #

作者:@Neo-Zhixing

Bevy 现在可以使用可选的 WireframePlugin 绘制线框。

wireframe

这些线框可以全局启用,也可以通过添加新的 Wireframe 组件来针对每个实体启用。

简单的 3D 游戏示例:外星人蛋糕瘾君子 #

作者:@mockersf

此示例简要介绍了在 Bevy 中构建 3D 游戏的方法。它展示了如何生成场景、响应输入、实现游戏逻辑以及处理状态转换。尽可能多地收集蛋糕吧!

alien_cake_addict

计时器改进 #

作者:@kokounet

Timer 结构体现在内部使用 Duration,而不是使用 f32 表示秒。这既提高了精度,也使 API 看起来更美观。

fn system(mut timer: ResMut<Timer>, time: Res<Time>) {
    if timer.tick(time.delta()).just_finished() {
        println!("timer just finished");
    }
}

资产改进 #

作者:@willcrichton, @zicklag, @mockersf, @Archina

Bevy 的资产系统在此版本中进行了一些小改进。

  • Bevy 在加载资产时不再出现错误 panic。
  • 现在正确处理包含多个点的资产路径。
  • 改进了由资产加载器生成的“带标签资产”的类型安全性。
  • 使资产路径加载不区分大小写。

WGPU 配置选项 #

作者:@Neo-Zhixing

现在可以通过在 WgpuOptions 资源中设置它们来启用/禁用 wgpu 功能(例如 WgpuFeature::PushConstantsWgpuFeature::NonFillPolygonMode)。

app
    .insert_resource(WgpuOptions {
        features: WgpuFeatures {
            features: vec![WgpuFeature::NonFillPolygonMode],
        },
        ..Default::default()
    })

wgpu 限制(例如 WgpuLimits::max_bind_groups)现在也可以在 WgpuOptions 资源中配置。

场景实例实体迭代 #

作者:@mockersf

现在可以迭代已生成场景实例中的所有实体。这使得在场景加载完成后可以对它们进行后处理。

struct MySceneInstance(InstanceId);

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut scene_spawner: ResMut<SceneSpawner>,
) {
    // Spawn a scene and keep its `instance_id`
    let instance_id = scene_spawner.spawn(asset_server.load("model.gltf#Scene0"));
    commands.insert_resource(MySceneInstance(instance_id));
}

fn print_scene_entities(
    scene_spawner: Res<SceneSpawner>,
    scene_instance: Res<MySceneInstance>,
) {
    if let Some(entity_iter) = scene_spawner.iter_instance_entities(scene_instance.0) {
        for entity in entity_iter {
            println!("Found scene entity {:?}", entity);
        }
    }
}

窗口调整大小约束 #

作者:@digital7-code

窗口现在可以具有“调整大小约束”。窗口不能调整到超过这些约束的大小。

app
    .insert_resource(WindowDescriptor {
        resize_constraints: WindowResizeConstraints {
            min_height: 200.0,
            max_height: 800.0,
            ..Default::default()
        },
        ..Default::default()
    })

!发送任务 #

作者:@alec-deason

Bevy 的异步任务系统现在支持 !Send 任务。有些任务不能发送/运行在其他线程上(例如,由即将发布的 Distill 资产插件创建的任务)。“线程局部”任务现在可以在 Bevy TaskPools 中生成,如下所示。

let pool = TaskPool::default();
pool.scope(|scope| {
    scope.spawn_local(async move {
        println!("I am a local task");
    });
});

更多 ECS V2 更改 #

EntityRef / EntityMut #

作者:@cart

在 **Bevy 0.4** 中,世界实体操作需要用户在每个操作中传递 entity ID。

let entity = world.spawn((A, )); // create a new entity with A
world.get::<A>(entity);
world.insert(entity, (B, C));
world.insert_one(entity, D);

这意味着每个操作都需要查找实体位置/验证其有效性。初始生成操作还需要 Bundle 作为输入。当不需要组件(或只需要一个组件)时,这可能很麻烦。

这些操作已被 EntityRefEntityMut 取代,它们是围绕世界的“构建器式”包装器,提供对单个预验证实体的读和读/写操作。

// spawn now takes no inputs and returns an EntityMut
let entity = world.spawn()
    .insert(A) // insert a single component into the entity
    .insert_bundle((B, C)) // insert a bundle of components into the entity
    .id() // id returns the Entity id

// Returns EntityMut (or panics if the entity does not exist)
world.entity_mut(entity)
    .insert(D)
    .insert_bundle(SomeBundle::default());

// The `get_X` variants return Options, in case you want to check existence instead of panicking 
world.get_entity_mut(entity)
    .unwrap()
    .insert(E);

if let Some(entity_ref) = world.get_entity(entity) {
    let d = entity_ref.get::<D>().unwrap();
}

Commands 也已更新以使用这种新模式。

let entity = commands.spawn()
    .insert(A)
    .insert_bundle((B, C))
    .insert_bundle(SomeBundle::default())
    .id();

Commands 也仍然支持使用 Bundle 进行生成,这应该使从 **Bevy 0.4** 迁移更容易。在某些情况下,它还可以减少样板代码。

commands.spawn_bundle(SomeBundle::default());

请注意,这些 Command 方法使用“类型状态”模式,这意味着这种类型的链式操作不再可能。

// Spawns two entities, each with the components in SomeBundle and the A component
// Valid in Bevy 0.4, but invalid in Bevy 0.5
commands
    .spawn(SomeBundle::default())
    .insert(A)
    .spawn(SomeBundle::default())
    .insert(A);

相反,您应该这样做。

commands
    .spawn_bundle(SomeBundle::default())
    .insert(A);
commands
    .spawn_bundle(SomeBundle::default())
    .insert(A);

这使我们能够使“实体 ID 检索”变得可靠,并为将来的 API 改进打开了大门。

Query::single #

作者:@TheRawMeatball

查询现在具有 Query::singleQuery::single_mut 方法,如果恰好只有一个匹配的实体,则这些方法会返回一个查询结果。

fn system(query: Query<&Player>) {
    // only returns Ok if there is exactly one Player
    if let Ok(player) = query.single() {
    }
}

删除 ChangedRes #

作者:@TheRawMeatball

我们已经删除了 ChangedRes<A>,转而使用以下方法。

fn system(a: Res<A>) {
    if a.is_changed() {
        // do something
    }
}

可选资源查询 #

作者:@jamadazi

现在,系统可以通过 Option 查询来检查资源是否存在。

fn system(a: Option<Res<A>>) {
    if let Some(a) = a {
        // do something
    }
}

新的 Bundle 命名约定 #

组件 Bundle 以前使用 XComponents 命名约定(例如:SpriteComponentsTextComponents 等)。我们决定改为使用 XBundle 命名约定(例如:SpriteBundleTextBundle 等),以便更明确地说明这些类型的含义,并帮助防止新用户将 Bundle 和组件混淆。

世界元数据改进 #

作者:@cart

World 现在具有可查询的 ComponentsArchetypesBundlesEntities 集合。

// you can access these new collections from normal systems, just like any other SystemParam
fn system(archetypes: &Archetypes, components: &Components, bundles: &Bundles, entities: &Entities) {
}

这使开发者能够从他们的系统访问内部 ECS 元数据。

可配置的 SystemParams #

作者:@cart, @DJMcNab

用户现在可以为系统参数提供一些初始配置/值(如果可能)。大多数 SystemParams 没有配置(配置类型为 ()),但 Local<T> 参数现在支持用户提供的参数。

fn foo(value: Local<usize>) {    
}

app.add_system(foo.system().config(|c| c.0 = Some(10)));

为脚本支持做准备 #

作者:@cart

Bevy ECS 组件现在与 Rust 类型分离。新的 Components 集合存储元数据,例如内存布局和析构函数。组件也不再需要 Rust TypeIds。

可以使用 world.register_component() 随时添加新的组件元数据。

所有组件存储类型(目前为 Table 和 Sparse Set)都是“blob 存储”。它们可以存储任何具有给定内存布局的值。这使得来自其他来源(例如:Python 数据类型)的数据可以像 Rust 数据类型一样存储和访问。

我们尚未完全启用脚本 (并且可能永远不会正式支持非 Rust 脚本),但这朝着启用社区支持的脚本语言迈出了重要一步。

将资源合并到世界中 #

作者:@cart

资源现在只是一种特殊的组件。这使我们能够通过重用现有的 Bevy ECS 内部结构来保持代码规模较小。它还使我们能够优化并行执行器访问控制,并且应该使脚本语言集成更容易。

world.insert_resource(1);
world.insert_resource(2.0);
let a = world.get_resource::<i32>().unwrap();
let mut b = world.get_resource_mut::<f64>().unwrap();
*b = 3.0;

// Resources are still accessed the same way in Systems
fn system(foo: Res<f64>, bar: ResMut<i32>) {
}

但是,此合并确实为直接与 World 交互的人创建了问题。如果您需要同时对多个资源进行可变访问,该怎么办?world.get_resource_mut() 可变地借用 World,这将阻止多个可变访问!我们使用 WorldCell 解决了这个问题。

WorldCell #

作者:@cart

WorldCell 应用了系统使用的“访问控制”概念来直接访问世界。

let world_cell = world.cell();
let a = world_cell.get_resource_mut::<i32>().unwrap();
let b = world_cell.get_resource_mut::<f64>().unwrap();

这添加了廉价的运行时检查,以确保世界访问不会相互冲突。

我们将其设置为一个单独的 API,以便用户可以决定他们想要进行哪些权衡。直接世界访问具有更严格的生命周期,但效率更高,并且进行编译时访问控制。WorldCell 的生命周期更宽松,但因此会产生少量运行时开销。

API 目前仅限于资源访问,但将来会扩展到查询/实体组件访问。

资源范围 #

作者:@cart

WorldCell 尚未支持组件查询,即使它支持,有时仍然会有正当理由需要可变世界引用可变资源引用(例如:bevy_render 和 bevy_scene 都需要这样做)。在这些情况下,我们始终可以降级到不安全的 world.get_resource_unchecked_mut(),但这并不是理想的!

相反,开发者可以使用“资源范围”。

world.resource_scope(|world: &mut World, mut a: Mut<A>| {
})

这将暂时从 World 中删除 A 资源,提供对两者的可变指针,并在完成时将 A 添加回 World。由于迁移到 ComponentIds/稀疏集,这是一个廉价的操作。

如果需要多个资源,则可以嵌套范围。如果这种模式变得很普遍,并且样板代码变得很糟糕,我们也可以考虑向 API 添加“资源元组”。

查询冲突使用 ComponentId 而不是 ArchetypeComponentId #

作者:@cart

出于安全原因,系统不能包含彼此冲突的查询,除非将它们包装在 QuerySet 中。在 **Bevy 0.4** 中,我们使用 ArchetypeComponentIds 来确定冲突。这很好,因为它可以考虑过滤器。

// these queries will never conflict due to their filters
fn filter_system(a: Query<&mut A, With<B>>, b: Query<&mut B, Without<B>>) {
}

但它也存在一个重大缺点。

// these queries will not conflict _until_ an entity with A, B, and C is spawned
fn maybe_conflicts_system(a: Query<(&mut A, &C)>, b: Query<(&mut A, &B)>) {
}

如果生成了具有 A、B 和 C 的实体,则上面的系统将在运行时出现 panic。这使得很难相信您的游戏逻辑将在不崩溃的情况下运行。

Bevy 0.5中,我们切换到使用ComponentId而不是ArchetypeComponentId。这样做确实更严格。maybe_conflicts_system现在总是会失败,但它会在启动时始终如一地失败。

简单来说,它也会禁止filter_system,这将是可用性上的重大降级。Bevy 有许多依赖于不相交查询的内部系统,我们预计它将在用户空间中成为一种常见的模式。为了解决这个问题,我们添加了一个新的内部FilteredAccess<T>类型,它包装了Access<T>并添加了带/不带过滤器。如果两个FilteredAccess具有证明它们不相交的带/不带值,它们将不再冲突。

这意味着filter_systemBevy 0.5中仍然是完全有效的。我们获得了旧实现的大部分优势,但同时在应用程序启动时执行了一致且可预测的规则。

Bevy 的未来? #

我们还有很长的路要走,但 Bevy 开发者社区正在迅速发展,我们已经对未来有了宏伟的计划。预计很快将在以下领域取得进展

  • "流水线"渲染和其他渲染器优化
  • Bevy UI 重构
  • 动画:组件动画和 3D 骨骼动画
  • ECS:关系/索引,异步系统,原型不变性,"无阶段"系统调度
  • 3D 照明功能:阴影,更多灯光类型
  • 更多 Bevy 场景功能和可用性改进

我们还计划在最终确定 Bevy UI 设计后立即开始 Bevy 编辑器的开发工作。

支持 Bevy #

赞助 有助于 Bevy 的全职工作持续发展。如果你相信 Bevy 的使命,请考虑赞助 @cart ... 每一份帮助都很重要!

捐赠 心形图标

贡献者 #

衷心感谢88 位贡献者,他们使此次发布(以及相关的文档)成为可能!

  • mockersf
  • CAD97
  • willcrichton
  • Toniman20
  • ElArtista
  • lassade
  • Divoolej
  • msklywenn
  • cart
  • maxwellodri
  • schell
  • payload
  • guimcaballero
  • themilkybit
  • Davier
  • TheRawMeatball
  • alexschrod
  • Ixentus
  • undinococo
  • zicklag
  • lambdagolem
  • reidbhuntley
  • enfipy
  • CleanCut
  • LukeDowell
  • IngmarBitter
  • MinerSebas
  • ColonisationCaptain
  • tigregalis
  • siler
  • Lythenas
  • Restioson
  • kokounet
  • ryanleecode
  • adam-bates
  • Neo-Zhixing
  • bgourlie
  • Telzhaak
  • rkr35
  • jamadazi
  • bjorn3
  • VasanthakumarV
  • turboMaCk
  • YohDeadfall
  • rmsc
  • szunami
  • mnmaita
  • WilliamTCarroll
  • Ratysz
  • OptimisticPeach
  • mtsr
  • AngelicosPhosphoros
  • Adamaq01
  • Moxinilian
  • tomekr
  • jakobhellermann
  • sdfgeoff
  • Byteron
  • aevyrie
  • verzuz
  • ndarilek
  • huhlig
  • zaszi
  • Puciek
  • DJMcNab
  • sburris0
  • rparrett
  • smokku
  • TehPers
  • alec-deason
  • Fishrock123
  • woubuc
  • Newbytee
  • Archina
  • StarArawn
  • JCapucho
  • M2WZ
  • TotalKrill
  • refnil
  • bitshifter
  • NiklasEi
  • alice-i-cecile
  • joshuajbouw
  • DivineGod
  • ShadowMitia
  • memoryruins
  • blunted2night
  • RedlineTriad

变更日志 #

新增 #