Bevy 0.9

发布日期:2022 年 11 月 12 日 作者:Carter Anderson ( 一只戴着猫耳朵、挥舞着触手的卡通人物,即 GitHub 的吉祥物和标志 @cart 一只灰色的鸟飞行的矢量图;X(前 Twitter)的旧标志 @cart_cart 一个指向右边的三角形在一个圆角矩形中;YouTube 的标志 cartdev )

An image representing the article

感谢 **159** 位贡献者、**430** 个拉取请求、社区审阅者以及我们的 慷慨的赞助者,我很高兴在 crates.io 上宣布发布 **Bevy 0.9**!

对于那些不知道的人来说,Bevy 是一个用 Rust 构建的清新简洁、数据驱动的游戏引擎。你可以查看我们的 快速入门指南 以立即尝试它。它是免费的,并且永远开源!你可以在 GitHub 上获取完整的 源代码。查看 Bevy 资产 以获取社区开发的插件、游戏和学习资源的集合。

要将现有的 Bevy App 或插件更新到 **Bevy 0.9**,请查看我们的 0.8 到 0.9 迁移指南.

自从我们几个月前发布上一个版本以来,我们已经添加了很多新的功能、错误修复和质量改进,但以下是其中的一些亮点

  • HDR 后处理、色调映射和光晕:Bevy 拥有一个新的 HDR 后处理和色调映射管道,我们使用它来实现“光晕”后处理效果!
  • FXAA:添加了快速近似抗锯齿,它为用户提供了一种新的廉价屏幕空间抗锯齿选项。
  • 去带抖动:使用这种新的后处理效果来隐藏梯度精度错误!
  • 其他后处理改进:视图目标双缓冲和自动渲染目标格式处理。
  • 新的场景格式:Bevy 的新场景格式更小、更容易手动组成,也更容易阅读。它有“人类可读”和“二进制”两种变体!
  • 代码驱动的场景构建:使用查询和特定实体引用,从现有的应用程序动态构建场景。
  • 改进的实体/组件 API:现在,使用组件生成实体比以往任何时候都更简单、更符合人体工程学!
  • 独占系统重构:独占系统(具有唯一 ECS 世界访问权限的系统)现在只是“普通”系统,具有显着改善的可用性。
  • 枚举反射:Bevy Reflect 现在可以反映枚举类型,这将它们公开给 Bevy 的场景系统,并为枚举打开编辑器工具的大门。
  • 时间着色器全局变量:时间现在作为全局变量传递给着色器,使得在自定义着色器中进行时间驱动的动画变得容易!
  • 插件设置:插件现在可以拥有设置,这些设置可以在插件组中被覆盖,从而简化插件配置故事。
  • Bevy UI Z 索引:使用局部和全局 Z 索引来控制 UI 元素如何堆叠在彼此之上

HDR 后处理、色调映射和光晕 #

作者:@ChangeCaps、@jakobhellermann、@cart、@JMS55

Bevy 现在支持“光晕”后处理效果,它由对我们的 HDR(高动态范围)渲染管道的许多内部改进提供支持。

bloom

光晕在明亮的光线周围产生“模糊”效果,它模拟了相机(以及我们的眼睛)在现实世界中感知光线的方式。高质量的光晕建立在 HDR 渲染管道之上,HDR 渲染管道使用超过标准 8 位每通道(rgba)来表示光线和颜色,而标准 8 位每通道(rgba)在其他地方使用。在之前的版本中,Bevy 已经在其 PBR 着色器中 内部进行 HDR 照明,但由于我们将渲染结果渲染到“普通”(低动态范围)纹理中,因此在将 HDR 照明映射到 LDR 纹理(使用称为色调映射的过程)时,我们必须丢失额外的 HDR 信息。

在 **Bevy 0.9** 中,你现在可以配置相机以渲染到 HDR 纹理,这样在完成“主要通道”渲染后,将保留高动态范围信息

Camera {
    // Currently this defaults to false, but we will likely
    // switch this to true by default in future releases
    hdr: true,
    ..default()
}

这使得后处理效果(如光晕)能够访问原始 HDR 信息。当启用 HDR 纹理时,我们将“色调映射”延迟到“HDR 后处理效果”在我们的 渲染图 中运行之后。

通过将 BloomSettings 组件添加到启用 HDR 纹理的相机来启用光晕效果

commands.spawn((
    Camera3dBundle {
        camera: Camera {
            hdr: true,
            ..default()
        },
        ..default()
    },
    BloomSettings::default(),
));

如果配置错误,光晕效果可能会过于强烈。 BloomSettings 具有许多选项来对其进行调整,但最重要的是 intensity,它可以(也应该)用于调整应用效果的程度。

说真的……这种效果可能很讨厌

too much bloom

在大多数情况下,最好保持微妙。

HDR 渲染也可以在 2D 中使用,这意味着你也可以在 2D 中使用光晕效果!

2D bloom

FXAA:快速近似抗锯齿 #

作者:@DGriffin91、@cart

**Bevy 0.9** 添加了对 FXAA(快速近似抗锯齿)的支持。FXAA 是一种流行的(且廉价的!)抗锯齿方法,它使用亮度数据对比度来识别边缘并对其进行模糊处理

no_aa fxaa

Bevy 已经支持 MSAA(多重采样抗锯齿),MSAA 在渲染几何体边缘时进行多次采样,这使得这些边缘更加清晰

msaa

选择抗锯齿实现都是权衡取舍

  • MSAA:清晰、高质量的几何体边缘。保留图像的其他部分(如纹理和阴影)不受影响,这可能是一个优点(更清晰的输出)或缺点(更多锯齿)。比 FXAA 更昂贵。
  • FXAA:在模糊时考虑整个图像,包括纹理,这可能是一个优点(纹理和阴影得到抗锯齿处理)或缺点(图像整体变得更加模糊)。运行成本低(对于移动或 Web AA 来说是一个不错的选择)。

现在,我们的后处理管道已经成熟,我们计划在未来的 Bevy 版本中添加更多抗锯齿选项。我们已经开始开发 TAA(时间抗锯齿)和 SMAA(亚像素形态抗锯齿)实现!

去带抖动 #

作者:@aevyrie

“颜色带状”是使用 8 位颜色通道(几乎所有设备/屏幕都需要)时已知的限制。

当尝试为低噪声纹理(例如,“纯绿色”材质的照明)渲染平滑梯度时,这一点最为明显

banding

如果你仔细观察绿色平面 *或* 棕褐色立方体,你会注意到每个颜色阴影都有明显的条带。解决此问题的流行方法是对最终图像进行“抖动”。

Bevy 0.9 现在在色调映射阶段默认执行“去带抖动”

debanding

你可以按相机启用和禁用它

  commands.spawn(Camera3dBundle {
      tonemapping: Tonemapping::Enabled {
          deband_dither: true,
      },
      ..default()
  });

后处理:视图目标双缓冲 #

作者:@cart

渲染后处理效果需要输入纹理(包含“当前”渲染)和输出纹理(应用效果后的“新”渲染)。Bevy 的先前版本只有一个主要的“视图目标”图像。这意味着,在简单情况下,后处理效果需要管理并渲染到它们自己的“中间”纹理,然后将其 *写回* 主要目标。这显然效率低下,因为我们为每个效果都有一个新的纹理分配 *并且* 我们需要额外的工作将中间纹理复制回主要纹理。

为了解决这个问题,在 **Bevy 0.9** 中,我们现在对视图目标纹理进行“双缓冲”,这意味着我们拥有它们的两个副本,我们在这两个副本之间切换。在给定的时间点,一个副本是当前的“主要”纹理,而另一个副本是“下一个”主要纹理。后处理效果开发人员现在可以触发“后处理写入”,这将返回一个 sourcedestination 纹理。它假定效果将 source 写入 destination(有或没有修改)。 destination 然后将成为新的“主要”纹理。

let post_process = view_target.post_process_write();
render_some_effect(render_context, post_process.source, post_process.destination);

这减少了后处理效果开发人员的复杂性负担,并使我们的管道保持良好且高效。新的 FXAA 效果 是使用这个新系统实现的。后处理插件开发人员可以使用该实现作为参考。

改进的渲染目标纹理格式处理 #

作者:@VitalyAnkh、@cart

**Bevy 0.9** 现在检测并使用每个窗口/表面的首选 TextureFormat,而不是使用硬编码的编译时选定的每平台格式。这意味着我们自动支持不常见的平台和配置。此外,Bevy 的主要通道和后处理通道现在渲染到稳定/一致的 TextureFormats(例如:Rgba16Float 用于 HDR)。我们从这些“标准”纹理到最终渲染目标的首选格式执行最终的 blit。这简化了渲染管道构建,允许跨渲染目标(即使它们的格式不匹配)重复使用渲染管道,并提供一致且可预测的渲染管道行为。

这也意味着,在渲染到纹理时,纹理格式不再需要与表面的纹理格式匹配。例如,你现在可以渲染到仅具有红色通道的纹理

render to texture red

新的场景格式 #

作者:@MrGVSV

**Bevy 0.9** 引入了一种 *大有改进* 的场景格式,它使场景更小、更容易手动组成,也更容易阅读。这是由对 Bevy Reflect(Bevy 的 Rust 运行时反射系统)的许多改进所支持的。对 Bevy 场景格式的大多数改进实际上是对所有 Bevy Reflect 序列化的通用改进!

// The New Bevy Scene Format
(
  entities: {
    0: (
      components: {
        "game::Player": (
          name: "Reyna",
          position: (
            x: 0.0,
            y: 0.0,
          ),
        ),
        "game::Health": (
          current: 5,
          max: 10,
        ),
        "game::Team": A,
      },
    ),
    1: (
      components: {
        "game::Player": (
          name: "Sova",
          position: (
            x: 10.0,
            y: 0.0,
          ),
        ),
        "game::Health": (
          current: 10,
          max: 10,
        ),
        "game::Team": B,
      },
    ),
  },
)

将其与旧格式进行比较

// The Old Bevy Scene Format
[
  (
    entity: 0,
    components: [
      {
        "type": "game::Player",
        "struct": {
          "name": {
            "type": "alloc::string::String",
            "value": "Reyna",
          },
          "position": {
            "type": "glam::f32::vec2::Vec2",
            "struct": {
              "x": {
                "type": "f32",
                "value": 0.0,
              },
              "y": {
                "type": "f32",
                "value": 0.0,
              },
            },
          },
        },
      },
      {
        "type": "game::Health",
        "struct": {
          "current": {
            "type": "usize",
            "value": 5,
          },
          "max": {
            "type": "usize",
            "value": 10,
          },
        },
      },
      {
        "type": "game::Team",
        "value": A,
      },
    ],
  ),
  (
    entity: 1,
    components: [
      {
        "type": "game::Player",
        "struct": {
          "name": {
            "type": "alloc::string::String",
            "value": "Sova",
          },
          "position": {
            "type": "glam::f32::vec2::Vec2",
            "struct": {
              "x": {
                "type": "f32",
                "value": 10.0,
              },
              "y": {
                "type": "f32",
                "value": 0.0,
              },
            },
          },
        },
      },
      {
        "type": "game::Health",
        "struct": {
          "current": {
            "type": "usize",
            "value": 10,
          },
          "max": {
            "type": "usize",
            "value": 10,
          },
        },
      },
      {
        "type": "game::Team",
        "value": B,
      },
    ],
  ),
]

有如此多的改进,你可能很难全部挑出来!

更简单的结构体语法 #

结构体现在使用结构体格式,而不是复杂的基于 map 的表示。

// Old
{
    "type": "game::Health",
    "struct": {
        "current": {
            "type": "usize",
            "value": 5,
        },
        "max": {
            "type": "usize",
            "value": 10,
        },
    },
},

// New
"game::Health": (
    current: 5,
    max: 10,
),

更简单的原始序列化 #

类型现在可以选择直接使用 serde 序列化,这使得原始值更容易操作。

// Old
"name": {
    "type": "alloc::string::String",
    "value": "Reyna",
},

// New
name: "Reyna",

更友好的枚举语法 #

考虑枚举

pub enum Team {
    A,
    B,
}

让我们比较一下它的序列化方式

// Old
{
    "type": "game::Team",
    "value": A,
},

// New
"game::Team": A,

另外需要注意的是,Bevy Reflect 直到 **Bevy 0.9** 才直接支持枚举。旧版本的 Bevy 需要将 #[reflect_value] 与普通的 serde 结合使用来处理枚举,这要复杂得多。有关详细信息,请参阅本博文中的 枚举反射 部分!

更友好的元组 #

// Old
{
  "type": "(f32, f32)",
  "tuple": [
    {
      "type": "f32",
      "value": 1.0
    },
    {
      "type": "f32",
      "value": 2.0
    }
  ]
}

// New
{
  "(f32, f32)": (1.0, 2.0)
}

顶级结构体 #

Bevy 场景现在有一个顶级结构体,这使得我们能够在未来向 Bevy 场景格式添加额外的值和元数据(例如版本号、ECS 资源、资产等)。

// Old
[
    /* entities here */
]

// New
(
    entities: (
        /* entities here */
    )
)

在适当的地方使用 Map #

实体 ID 和组件值在 Bevy ECS 中必须是唯一的。为了更好地体现这一点,我们现在使用 map 语法,而不是列表。

// Old
[
  (
    entity: 0,
    components: [ ],
  ),
  (
    entity: 1,
    components: [ ],
  ),
]

// New
(
  entities: {
    0: (
      components: { },
    ),
    1: (
      components: { },
    ),
  },
)

二进制场景格式 #

作者:@MrGVSV

Bevy 场景可以序列化和反序列化为/从二进制格式,例如 bincodepostcardrmp_serde。这需要在新的场景格式中添加对“非自描述”格式的支持。

在 postcard 的情况下,这可能小近 5 倍(对于上面的场景,小了 4.53 倍)!如果你试图保持场景在磁盘上的大小,或者通过网络发送场景,这非常有用。

动态场景构建器 #

作者:@mockersf

Bevy 场景现在可以使用新的 DynamicSceneBuilder 动态构建。旧版本的 Bevy 已经支持 将“整个世界”写入场景,但在某些情况下,用户可能只想将特定实体写入场景。**Bevy 0.9** 的 DynamicSceneBuilder 使这成为可能

// Write players to a scene
fn system(world: &World, players: Query<Entity, With<Player>>) {
  let builder = DynamicSceneBuilder::from_world(world);
  builder.extract_entities(players.iter());
  let dynamic_scene = builder.build();
}

extract_entities 接受任何 Entity 迭代器。

你也可以传入特定的实体

builder.extract_entity(entity);

更多场景构建工具 #

作者:@mockersf

Scenes 现在可以克隆

let scene = scene.clone_with(type_registry).unwrap();

DynamicScenes 现在可以转换为 Scenes

let scene = Scene::from_dynamic_scene(dynamic_scene, type_registry).unwrap();

改进的实体/组件 API #

作者:@DJMcNab、@cart

使用组件生成实体,以及从实体添加/删除组件变得更加容易!

首先是一些快速的基础知识:Bevy ECS 使用 Components 来向实体添加数据和逻辑。为了使实体组合更轻松,Bevy ECS 还具有 Bundles,它定义了要一起添加的组件组。

就像 Bevy 的先前版本一样,Bundle 可以是组件的元组

(Player { name: "Sova" }, Health::new(10), Team::A)

Bundle 特性也可以推导出来

#[derive(Bundle)]
struct PlayerBundle {
  player: Player,
  health: Health,
  team: Team,
}

在 **Bevy 0.9** 中,Component 类型现在自动实现了 Bundle 特性,这使我们能够将所有实体组件操作整合到新的 spawninsertremove API 中。以前,我们有针对 Bundle(例如:insert_bundle(SomeBundle))和 Component(例如:.insert(SomeComponent))的不同变体。

Bundle 特性现在也为 Bundles 的元组实现,而不仅仅是 Components 的元组。这一点的价值将在稍后说明。

首先,spawn 现在接受一个 bundle

// Old (variant 1)
commands.spawn().insert_bundle(SpriteBundle::default());

// Old (variant 2)
commands.spawn_bundle(SpriteBundle::default());

// New
commands.spawn(SpriteBundle::default());

我们已经节省了一些字符,但我们才刚刚开始!因为 Component 实现了 Bundle,我们现在也可以将单个组件传入 spawn

// Old
commands.spawn().insert(Player { name: "Sova" });

// New
commands.spawn(Player { name: "Sova" });

当我们将 Bundle 元组引入其中时,事情变得更加有趣,这使我们能够将许多操作(涵盖组件和 bundle)合并到单个 spawn 调用中

// Old
commands
  .spawn_bundle(PlayerBundle::default())
  .insert_bundle(TransformBundle::default())
  .insert(ActivePlayer);

// New
commands.spawn((
  PlayerBundle::default(),
  TransformBundle::default(),
  ActivePlayer,
));

更容易键入和阅读。除此之外,从 Bevy ECS 的角度来看,这是一个单一的“bundle spawn”,而不是多个操作,这减少了 “原型移动”。这使得这个单一的生成操作更加高效!

这些原则也适用于 insert API。

// Old
commands
  .insert_bundle(PlayerBundle::default())
  .insert(ActivePlayer);

// New
commands.insert((PlayerBundle::default(), ActivePlayer));

它们也适用于 remove API。

// Old
commands
  .remove_bundle::<PlayerBundle>()
  .remove::<ActivePlayer>();

// New
commands.remove::<(PlayerBundle, ActivePlayer)>();

排他性系统重构 #

作者:@cart、@maniwani

为了为在最新合并的(但尚未实施的)无阶段 RFC 中概述的更大规模的调度程序更改做准备,我们已经开始模糊“排他性系统”(具有对 ECS World 的“排他性”完全可变访问权限的系统)和普通系统之间的界限,这些系统在历史上是具有严格界限的不同类型。

在 **Bevy 0.9** 中,排他性系统现在实现了正常的 System 特性!这最终将带来更大的影响,但在 **Bevy 0.9** 中,这意味着你不再需要在将排他性系统添加到调度程序时调用 .exclusive_system()

fn some_exclusive_system(world: &mut World) { }

// Old
app.add_system(some_exclusive_system.exclusive_system())

// New
app.add_system(some_exclusive_system)

我们还扩展了排他性系统以支持更多系统参数,这极大地改善了编写排他性系统的用户体验,并通过在执行之间缓存状态来提高效率。

SystemState 使得能够在排他性系统内部使用“普通”系统参数

// Old
fn some_system(world: &mut World) {
  let mut state: SystemState<(Res<Time>, Query<&mut Transform>)> =
      SystemState::new(&mut world);
  let (time, mut transforms) = state.get_mut(world);
}

// New
fn some_system(world: &mut World, state: &mut SystemState<(Res<Time>, Query<&mut Transform>)>) {
  let (time, mut transforms) = state.get_mut(world);
}

QueryState 使得能够缓存对单个查询的访问

// Old
fn some_system(world: &mut World) {
  let mut transforms = world.query::<&Transform>();
  for transform in transforms.iter(world) {
  }
}

// New
fn some_system(world: &mut World, transforms: &mut QueryState<&Transform>) {
  for transform in transforms.iter(world) {
  }
}

Local 使得能够在排他性系统内部存储本地数据

// Old
#[derive(Resource)]
struct Counter(usize);
fn some_system(world: &mut World) {
  let mut counter = world.resource_mut::<Counter>();
  counter.0 += 1;
}

// New
fn some_system(world: &mut World, mut counter: Local<usize>) {
  *counter += 1;
}

Bevy ECS 现在使用 GATS!#

作者:@BoxyUwU

Rust 1.65.0 稳定了 GATs(泛型关联类型),这使得我们能够显著简化 Bevy ECS 查询内部。

有一段时间,Bevy ECS 一直在用一个复杂的特性嵌套(WorldQueryWorldQueryGats(一个“缺少真正的 GATs”的特性)和 Fetch)来解决缺少 GATs 的问题。

在 **Bevy 0.9** 中,我们现在只有一个 WorldQuery 特性!这使得 Bevy ECS 更易于维护、扩展、调试、记录和理解。

推导资源 #

作者:@devil-ira、@alice-i-cecile

Resource 特性现在不再自动为所有类型实现。它必须推导出来

#[derive(Resource)]
struct Counter(usize);

这个改变是在 Component 类型做出相同决定 之后进行的。简而言之

  1. 自动为每种类型实现 Resource 使得非常容易意外地插入“错误”的值,例如插入构造函数指针而不是值本身

    struct Counter(usize);
    // This inserts the constructor function pointer as a resource!
    // Weird and confusing!
    app.insert_resource(Counter);
    // This is how it should be done!
    app.insert_resource(Counter(0));
    
  2. 推导 Resource 以结构化的方式记录意图。如果没有推导,资源性默认情况下是隐式的。

  3. 自动实现意味着插件可以在冲突的方式(例如:std 类型,例如 Vec<usize>)中使用相同的“通用”类型。默认情况下不实现意味着插件不能以冲突的方式使用这些通用类型。他们必须创建新的类型。

  4. 这为使用 Rust 类型系统配置资源类型打开了大门(就像我们已经对组件所做的那样)。

系统歧义解决 API 改进 #

作者:@JoJoJet、@alice-i-cecile

Bevy ECS 默认情况下会并行调度系统。它会安全地并行调度系统,遵守系统之间的依赖关系并强制执行 Rust 的可变性规则。默认情况下,这意味着如果系统 A 读取资源,而系统 B 写入资源(并且它们之间没有定义任何顺序),那么系统 A 可能会在系统 B之前之后执行。我们称这些系统为“歧义”系统。在某些情况下,这种歧义可能很重要,而在其他情况下可能无关紧要。

Bevy 已经有一个 系统歧义检测系统,它允许用户检测歧义系统并解决歧义(要么通过添加排序约束,要么通过忽略歧义)。用户可以将系统添加到“歧义集合”中,以忽略这些集合中系统之间的歧义

#[derive(AmbiguitySet)]
struct AmbiguousSystems;

app
  .add_system(a.in_ambiguity_set(AmbiguousSystems))
  .add_system(b.in_ambiguity_set(AmbiguousSystems))

这有点难以理解,并且比必要时引入了更多的样板代码。

在 **Bevy 0.9** 中,我们已经用更简单的 ambiguous_with 调用替换了歧义集合

app
  .add_system(a)
  .add_system(b.ambiguous_with(a))

这基于现有的 [SystemLabel] 方法,这意味着你也可以使用标签来实现“集合式”歧义解决

#[derive(SystemLabel)]
struct Foo;

app
  .add_system(a.label(Foo))
  .add_system(b.label(Foo))
  .add_system(b.ambiguous_with(Foo))

Bevy ECS 优化 #

作者:@james7132、@JoJoJet

我们在 **Bevy 0.9** 中获得了巨大的性能提升,这要归功于 @james7132

@JoJoJet 还优化了 Query::get_many 访问,方法是使用循环替换 array::map,将 get_many 优化了约 20-30%!

ECS 更改检测旁路 #

作者:@alice-i-cecile

Bevy ECS 通过一些非常精妙的 Rust 用法自动检测对组件和资源的更改。

但是,有时用户可能会进行他们不希望被检测到的更改。在 **Bevy 0.9** 中,更改检测现在可以绕过

fn system(mut transforms: Query<&mut Transform>) {
  for transform in &mut transforms {
    transform.bypass_change_detection().translation.x = 1.0;
  }
}

枚举反射 #

作者:@MrGVSV、@Davier、@nicopap

Bevy Reflect 现在原生支持 Rust 枚举!Bevy Reflect 是 Bevy 的“Rust 反射系统”,它允许我们在运行时动态地访问值的 Rust 类型信息。

在 Bevy 的早期版本中,我们需要通过将枚举类型视为“反射值”来绕过 Bevy Reflect 缺乏枚举支持的限制,这需要为每种类型做更多工作,而且它提供的关于该类型的反射信息更少。

// Old
#[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize, Reflect)]
#[reflect_value(PartialEq, Serialize, Deserialize)]
enum SomeEnum {
  A,
  B(usize),
  C {
    foo: f32,
    bar: bool,
  },
}

// New
#[derive(Reflect)]
enum SomeEnum {
  A,
  B(usize),
  C {
    foo: f32,
    bar: bool,
  },
}

不再需要魔法咒语了!

就像其他反射类型一样,枚举反射提供了许多新的运行时功能。

// Access variant names
let value = SomeEnum::A;
assert_eq!("A", value.variant_name());

// Get the variant type
match value.variant_type() {
  VariantType::Unit => {},
  VariantType::Struct => {},
  VariantType::Tuple => {},
}

let mut value = SomeEnum::C {
  foo: 1.23,
  bar: false
};

// Read/write specific fields by name
*value.field_mut("bar").unwrap() = true;

// Iterate over the entire collection of fields
for field in value.iter_fields() {
}

// Detect the value type and retrieve information about it
if let TypeInfo::Enum(info) = value.type_info() {
  if let VariantInfo::Struct(struct_info) = value.variant("C") {
    let first_field = struct_info.field_at(0).unwrap();
    assert_eq!(first_field.name(), "foo");
  }
}

在枚举上派生 Reflect 也会自动添加对“基于反射的序列化”的支持,从 Bevy 0.9 开始,现在 有了一个友好的语法

其他 Bevy Reflect 改进 #

作者:@MrGVSV,@makspll,@Shatur,@themasch,@NathanSWard

我们对 Bevy Reflect 做了许多其他改进!

“容器”反射特征(Map、List、Array、Tuple)现在可以被清空以获取所有权值。

let container: Box<dyn List> = Box::new(vec![1.0, 2.0]);
let values: Vec<Box<dyn Reflect>> = container.drain();

反射字段现在可以选择不进行序列化,而不会选择不进行整个反射。

#[derive(Reflect)]
struct Foo {
  a: i32,
  // fully invisible to reflection, including serialization
  #[reflect(ignore)]
  b: i32,
  // can still be reflected, but will be skipped when serializing
  #[reflect(skip_serializing)]
  c: i32,
}

装箱的“反射类型”特征(Struct、Enum、List 等)现在可以转换为更通用的 Box<dyn Reflect>

let list: Box<dyn List> = Box::new(vec![1.0, 2.0]);
let reflect: Box<dyn Reflect> = list.into_reflect();

现在可以获取反射类型的拥有所有权的变体。

let value: Box<Sprite> = Box::new(Sprite::default());
if let ReflectOwned::Struct(owned) = value.reflect_owned() {
  // owned is a Box<dyn Struct>
}

“反射路径 API”中的数组现在可以使用列表语法。

#[derive(Reflect)]
struct Foo {
    bar: [u8; 3],
}

let foo = Foo {
  bar: [10, 20, 30],
};

assert_eq!(*foo.get_path("bar[1]").unwrap(), 20);

反射的 List 现在有一个 pop 操作。

let mut list: Box<dyn List> = Box::new(vec![1u8, 2u8]);
let value: Box<dyn Reflect> = list.pop().unwrap();
assert_eq!(*value.downcast::<u8>().unwrap(), 2u8);

示例:游戏手柄查看器 #

作者:@rparrett

Bevy 现在有一个游戏手柄输入查看器应用程序,它可以使用 cargo run --example gamepad_viewer 从 Bevy 仓库运行。

轴和按钮设置验证 #

作者:@mfdorst,@targrub

[InputAxis] 和 [ButtonSettings] 现在使用 getter 和 setter 来确保设置的完整性。setter 会返回错误,而不是允许无效状态。

例如,尝试将按钮的“按下阈值”设置为低于“释放阈值”的值会导致错误。

button_settings.set_release_threshold(0.65);
// this is too low!
assert!(button_settings.try_set_press_threshold(0.6).is_err())

ScanCode 输入资源 #

作者:@Bleb1k

Bevy 0.9 添加了一个 Input<ScanCode> 资源,它像 Input<KeyCode> 一样,但忽略键盘布局。

fn system(scan_code: Res<Input<ScanCode>>, key_code: Res<Input<KeyCode>>) {
  // 33 is the scan code for F on a physical keyboard
  if scan_code.pressed(ScanCode(33)) {
    log!("The physical F key is pressed on the keyboard");
  }
  
  if keycode.pressed(KeyCode::F) {
    log!("The logical F key is pressed on the keyboard, taking layout into account.");
  }
}

时间着色器全局变量 #

作者:@IceSentry

Bevy 着色器终于可以访问内置时间值,无需用户手动计算和传递时间值。时间在着色器中非常有用,因为它为动画化值打开了大门。

这是一个使用时间在黑色和红色之间进行动画化的简单着色器。

@fragment
fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
    return vec4<f32>(sin(globals.time * 10.0), 0.0, 0.0, 1.0);
}

Bevy 着色器现在可以访问以下全局变量

  • time:自启动以来的时间(以秒为单位),在 1 小时后重置为 0。
  • delta_time:自上一帧以来的时间(以秒为单位)。
  • frame_count:自应用程序启动以来的帧数,在达到 u32 的最大值后重置为 0。

高实体渲染器减速优化 #

作者:@TheRawMeatball

Bevy 的渲染器 在“主世界”和“渲染世界”之间同步实体状态,这使得并行流水线渲染成为可能。为了实现这一点,我们每帧清除渲染实体以确保提取状态的完整性。

然而,很明显,我们用于每帧清除实体的方法会产生每实体的成本,在非常高的实体数量下,这种成本会变得很明显。

Bevy 0.9 中,我们显著优化了实体清除,将清除 5,000,000 个实体的成本从大约 360 微秒降低到大约 120 微秒。我们还在考虑一个“保留状态”提取模型,它依托于 Bevy ECS 的内置变更检测,这将完全消除清除实体的需要(并且更一般地优化提取过程)。但是,实现这一点将是一项更大的工作量!

顶点属性完全可选 #

作者:@IceSentry

在之前的版本中,我们 通过专门针对网格顶点属性使顶点属性可选。但我们保留了一些常用属性作为必需项:位置和法线。Bevy 0.9 完成了这项工作。现在所有标准网格顶点属性都完全可选。如果你的网格由于某种原因不需要位置,Bevy 不会阻止你!

公开多绘制间接 #

作者:@Neo-Zhixing

Wgpu 对支持它们的平台上的“多绘制间接” API 提供了选择性支持,这些 API 是实现高效“GPU 驱动渲染”的关键部分。Bevy 现在通过其“跟踪渲染通道”抽象公开这些 API,使开发者能够使用这些 API 构建渲染功能。

KTX2 数组/立方体贴图/立方体贴图数组纹理 #

作者:Rob Swain (@superdump)

Bevy 现在可以正确加载 KTX2 数组、立方体贴图和立方体贴图数组纹理资产,这为天空盒等场景打开了大门。

Bevy 还没有对天空盒提供高级支持,但我们有一个示例 说明用户如何实现此功能

Camera::viewport_to_world #

作者:@devil-ira

通常希望将“屏幕上”的位置转换为指向该位置从相机向外发出的射线。例如,如果你想在 3D 场景中点击某个东西来选择它,你可能会从相机视角中的那个点投射一条射线,看看它是否与场景中的任何“碰撞器”相交。

Bevy 相机现在有一个 viewport_to_world 函数,它提供了此功能。

let ray = camera.viewport_to_world(transform, cursor_position).unwrap();
if let Some(entity) = physics_context.cast_ray(ray.origin, ray.direction) {
  // select entity
}

以下由光标驱动的选择使用 viewport_to_world 计算从光标“射出”的射线,然后将其馈送到 bevy_rapier 物理库以检测和拾取光标下的卡片。

多个方向光 #

作者:@kurtkuehnert

Bevy 现在支持多个方向光(新限制为一次 10 个)。就像我们对点光源所做的那样,我们很可能在将来为支持存储缓冲区的平台使其无限制,但这只是朝着保持所有平台兼容性的方向迈出的第一步。

multiple directional

精灵矩形 #

作者:@inodentry

Sprites 现在可以定义“矩形”,这些矩形选择其纹理的特定区域用作“精灵”。

Sprite {
  rect: Some(Rect {
    min: Vec2::new(100.0, 0.0),
    max: Vec2::new(200.0, 100.0),
  }),
  ..default()
}

sprite rect

这类似于 TextureAtlasSprite / “精灵表”的工作方式,但不需要定义纹理图集。

插件设置 #

作者:@cart,@mockersf

在 Bevy 的早期版本中,“不可变”插件设置表示为普通的 ECS 资源,这些资源在插件初始化时被读取。这带来了许多问题。

  1. 如果用户在插件初始化后插入插件设置资源,它将被静默忽略(并使用默认值)。
  2. 用户可以在插件初始化后修改插件设置资源。这给用户造成了一种对无法更改的设置拥有控制权的错觉。

对于 WindowDescriptor 资源来说,这些问题尤其严重且令人困惑,但它是一个普遍问题。

为了解决这个问题,在 Bevy 0.9 中,我们将插件设置移到了插件本身,并创建了用于覆盖默认设置的新 API。

app.add_plugins(DefaultPlugins
  .set(AssetPlugin {
    watch_for_changes: true,
    ..default()
  })
  .set(WindowPlugin {
    window: WindowDescriptor {
      width: 400.0,
      ..default()
    },
    ..default()
  })
)

这使得设置和插件之间的联系变得清晰,并将这些“插件初始化”设置与“运行时可配置”设置(仍然表示为 ECS 资源)区分开来。

插件现在默认情况下是唯一的 #

作者:@mockersf

插件现在默认情况下是唯一的。尝试将唯一的插件多次添加到应用程序会导致错误。不打算唯一的插件可以覆盖默认的 is_unique 方法。

impl Plugin for MyPlugin {
  fn build(&self, app: &mut App) {
    app.add_system(some_system);
  }

  fn is_unique(&self) -> bool {
    false
  }
}

任务池:作用域上的嵌套生成 #

作者:@hymm

Bevy 的任务池现在支持“作用域上的嵌套生成”。

let results = task_pool.scope(|scope| {
    scope.spawn(async move {
        scope.spawn(async move { 1 });
        2
    });
});

assert!(results.contains(&1));
assert!(results.contains(&2));

这使得在执行其他任务时可以向任务池作用域添加新任务!这是实现新合并(但尚未实现)的 无阶段 RFC 的要求,但它为任何在 Bevy 中生成异步任务的人提供了新的模式!

任务池恐慌处理 #

作者:@james7132

Bevy 使用它自己的自定义异步任务池来管理调度并行、异步任务。在 Bevy 的早期版本中,如果任务在这些池中的一个中出现恐慌,则除非每个计划的任务使用 catch_unwind(这不可行),否则将无法恢复。这也将永久杀死全局任务池中的工作线程。

Bevy 0.9 通过在任务池执行器中调用 catch_unwind 来解决这个问题。

层次结构查询方法 #

作者:@devil-ira

为了便于导航层次结构,我们在 Query<&Children>Query<&Parent> 中添加了一些便捷方法。

#[derive(Resource)]
struct SomeEntity(Entity);

fn system(children: Query<&Children>, some_entity: Res<SomeEntity>) {
  // iterate all descendents of some_entity 
  for entity in children.iter_descendants(some_entity.0) {
  }
}

fn other_system(parents: Query<&Parent>, some_entity: Res<SomeEntity>) {
  // iterate all ancestors of some_entity 
  for entity in parents.iter_ancestors(some_entity.0) {
  }
}

Bevy UI:原点现在位于左上角 #

作者:@mahulst

Bevy UI 现在认为窗口的“左上角”为“原点”,它向“下方”扩展(Y 向下)。为了说明这一点,请考虑以下情况,其中在“默认”位置(原点)生成了一个小部件。

左上角原点(新) #

origin top left

左下角原点(旧) #

origin bottom left

我们之所以做出这个改变,是因为几乎整个 UI 生态系统都使用左上角作为原点(Web、Godot、GTK、GPU 图像等)。

在 Bevy 还处于起步阶段的早期,我(@cart)最初选择了左下角(Y 向上)以与 Bevy 的世界空间二维和三维坐标系保持一致。从理论上讲,我认为这将使所有事情更容易推理。但实际上,事实证明,这种一致性并没有给我们带来任何好处。而且当涉及 UI 默认行为时,这种行为违背了用户的预期。UI 倾向于向下扩展(从顶部开始),而不是向上扩展(从底部开始),因此覆盖默认值是一种常见的做法。

幸运的是,在 Bevy 0.9 中,我们现在与生态系统的其他部分保持一致!

Bevy UI:Z 索引 #

作者:@oceantume

Bevy UI 元素现在对它们的“Z 索引”(它们是“在前面”还是“在后面”)有了更多控制。在 Bevy 的早期版本中,这完全由层次结构决定:子项堆叠在父项和较早的兄弟姐妹之上。这是一个很好的“默认值”,适用于大部分 UI,但某些类型的 UI 需要对元素排序进行更多控制。

如果你是一名前端开发者,你曾经使用过 z-index CSS 属性,那么我们现在讨论的问题就是你遇到的问题。

Bevy 0.9 添加了一个新的 ZIndex 组件,它是一个枚举,包含两种模式。

  • ZIndex::Local(i32):覆盖相对于其兄弟姐妹的深度。
  • ZIndex::Global(i32):覆盖相对于 UI 根节点的深度。设置它实际上允许 UI 元素“逃离”相对于其父节点的 Z 顺序,而是相对于整个 UI 进行排序。

在上下文中(本地 vs 全局)具有较高 Z 级的 UI 项将显示在具有较低 Z 级的 UI 项的前面。Z 级的平局会回退到层次结构顺序。“后面的”子项堆叠在“前面的”子项之上。

为了说明这一点,请考虑以下 UI。

root (green)
  child1 (red)
  child2 (blue)

默认情况下,它们都具有 0 的 Z 索引。根节点位于底部,每个后续子项都“堆叠”在“顶部”。

z-index default

如果我们希望蓝色子节点堆叠在之前的红色子节点“后面”,我们可以将其 z-index 设置为小于默认值 0 的“局部”值。

blue.z_index = ZIndex::Local(-1);

z-index blue local

如果我们希望蓝色子节点堆叠在绿色根节点“后面”,我们可以将其 z-index 设置为小于默认值 0 的“全局”值。

blue.z_index = ZIndex::Global(-1);

z-index blue global

非常有用的东西!

Bevy UI 缩放 #

作者:@Weibye

Bevy UI 的全局“像素缩放”现在可以使用 UiScale 资源来设置。

// Render UI pixel units 2x bigger
app.insert_resource(UiScale { scale: 2.0 })

这允许开发人员在需要灵活性的情况下向用户公开任意缩放配置。

音频播放切换 #

作者:@lovelymono

现在可以切换音频播放,它将在播放和暂停之间切换。

// Old, manual toggling (still possible)
if audio_sink.is_paused() {
    audio_sink.play();
} else {
    audio_sink.pause();
}

// New, automatic toggling
audio_sink.toggle();

时间缩放 #

作者:@maniwani

现在可以在 Time 上配置“全局”时间缩放,这会缩放诸如 Time::delta_seconds() 之类的常用函数返回的值。

time.set_relative_speed(2.0);

在需要未缩放的值的情况下,可以使用这些函数的新的“原始”变体。

// The number of seconds elapsed since the last update, with time scaling taken into account.
let delta = time.delta_seconds();

// The number of seconds elapsed since the last update, with time scaling ignored.
let raw_delta = time.raw_delta_seconds();

时间包装 #

作者:@IceSentry

在某些情况下,例如着色器,需要将经过的时间值表示为 f32,这很快就会遇到精度问题。为了解决这个问题,Time 已扩展为支持“时间包装”。

// Wrap once every hour
time.wrapping_period = Duration::from_secs(60 * 60):

// If one hour and 6 seconds have passed since the app started,
// this will return 6 seconds.
let wrapped = time.seconds_since_startup_wrapped_f32();

下一步是什么? #

以下是一些事项。

  • 高级后期处理堆栈:现在我们已经有了核心后期处理管道,我们需要创建一个更高级的系统,使用户能够更轻松地选择、配置和重新排序每个摄像机的后期处理效果。此外,出于性能原因,我们希望尽可能将多个后期处理效果组合到一个通道中,因此我们需要一套能够促进这一过程的后期处理 API。
  • 更多后期处理效果:更多抗锯齿选项(TAA、SMAA)、更多色调映射算法选项(例如 ACES)、SSAO。
  • 资产预处理:我们将大力投入资产管道,重点关注以下方面。
    1. 预处理资产以在“开发期间”完成昂贵的操作,以便 Bevy 应用程序可以部署具有更漂亮、更小和/或加载速度更快的资产。
    2. 启用使用 .meta 文件配置资产。例如,您可以定义纹理压缩级别、它应该使用的过滤器或目标格式。
  • Bevy UI 改进:我们将继续改进 Bevy UI 的功能并扩展其窗口小部件库,重点关注启用编辑器体验。
  • 更多场景改进:嵌套场景、隐式默认值和内联资产。
  • Bevy 编辑器:我们将开始对 Bevy 编辑器体验进行原型设计,首先是场景编辑器工具。
  • 无阶段 ECS:现在 无阶段 RFC 已合并,我们可以开始实现无阶段调度!有关即将推出的改进概述,请参阅 RFC。这将是一场变革!

我们还在寻找一些关键领域的专家。我们目前的大多数开发人员都专注于上述工作,因此,如果您对以下领域感兴趣并有经验,我们很乐意与您取得联系!

  • 动画:动画混合、程序化动画和更高级别的动画系统。查看 GitHub 上标记为 A-Animation 的问题,并在我们 Discord 的 #animation-dev 频道上自我介绍。
  • 音频:我们需要对音频播放有更多控制,尤其是在叠加效果方面。查看 GitHub 上标记为 A-Audio 的问题,并在我们 Discord 的 #audio-dev 频道上自我介绍。

支持 Bevy #

赞助有助于使我们在 Bevy 上的工作可持续发展。如果您相信 Bevy 的使命,请考虑赞助我们……每一份帮助都很重要!

  • Carter Anderson (@cart):Bevy 的全职首席开发人员、项目经理和创建者。专注于构建核心引擎系统、指导项目方向和管理社区。
  • Alice Cecile (@alice-i-cecile):技术项目经理、疯狂科学家和文档主管。虽然她经常领导探索新领域的探险队,但 ECS 将永远是她的家。
  • François Mockers (@mockersf):CI 专家。确保一切顺利运行,并通过一次又一次的 PR 改进 Bevy。
  • Rob Swain (@superdump):光之掌控者。将数据变成闪耀的东西,并实现大规模并行化。目前正在业余时间进行黑客攻击,所以请捐赠或赞助团队的其他成员。❤️

贡献者 #

衷心感谢使此次发布(以及相关文档)成为可能的 159 位贡献者!按随机顺序列出。

  • @Edwox
  • @targrub
  • @fvacek
  • @xtr3m3nerd
  • @timokoesters
  • @Suficio
  • @Sergi-Ferrez
  • @hymm
  • @MrGVSV
  • @SleepySwords
  • @nicopap
  • @Vrixyz
  • @McSpidey
  • @VitalyAnkh
  • @ramirezmike
  • @jiftoo
  • @TheNeikos
  • @ManevilleF
  • @KDecay
  • @Zearin
  • @marlyx
  • @StarArawn
  • @Ixentus
  • @hmeine
  • @emersonmx
  • @gilescope
  • @inodentry
  • @robtfm
  • @yrns
  • @Lucidus115
  • @kurtkuehnert
  • @zmarlon
  • @leereilly
  • @galkowskit
  • @DGriffin91
  • @Ptrskay3
  • @strattonbrazil
  • @Pand9
  • @PROMETHIA-27
  • @zicklag
  • @lewiszlw
  • @contagnas
  • @EMachad0
  • @SpecificProtagonist
  • @BoxyUwU
  • @jkb0o
  • @xgbwei
  • @andresovela
  • @0x182d4454fb211940
  • @TehPers
  • @pcone
  • @CleanCut
  • @makspll
  • @64kramsystem
  • @Wandalen
  • @coreh
  • @Fracey
  • @Azervu
  • @SyamaMishra
  • @BeastLe9enD
  • @Weibye
  • @Pietrek14
  • @NiklasEi
  • @TheRawMeatball
  • @jgoday
  • @7flash
  • @light4
  • @Ian-Yy
  • @Carter0
  • @slyedoc
  • @devil-ira
  • @MDeiml
  • @NathanSWard
  • @robem
  • @Bleb1k
  • @bzm3r
  • @anchpop
  • @aevyrie
  • @amiani
  • @x3ro
  • @NoahShomette
  • @bjorn3
  • @djeedai
  • @bwhitt7
  • @oceantume
  • @micron-mushroom
  • @JMS55
  • @asherkin
  • @afonsolage
  • @shuoli84
  • @harudagondi
  • @Demiu
  • @TimJentzsch
  • @gak
  • @dataphract
  • @raffimolero
  • @Moulberry
  • @james7132
  • @torsteingrindvik
  • @jakobhellermann
  • @hakolao
  • @themasch
  • @CatThingy
  • @Metadorius
  • @merelymyself
  • @SludgePhD
  • @CGMossa
  • @sullyj3
  • @ian-h-chamberlain
  • @lain-dono
  • @mwcz
  • @thebluefish
  • @manokara
  • @mirkoRainer
  • @hankjordan
  • @cryscan
  • @WaffleLapkin
  • @mahulst
  • @AlexOkafor
  • @Davier
  • @jwagner
  • @CAD97
  • @alice-i-cecile
  • @james-j-obrien
  • @rparrett
  • @tguichaoua
  • @YohDeadfall
  • @msvbg
  • @komadori
  • @maniwani
  • @Shatur
  • @LarsDu
  • @DJMcNab
  • @JoJoJet
  • @polarvoid
  • @KirmesBude
  • @Aceeri
  • @ottah
  • @IceSentry
  • @Piturnah
  • @lovelymono
  • @maxwellodri
  • @oledfish
  • @BorisBoutillier
  • @mockersf
  • @Nilirad
  • @elbertronnie
  • @maccesch
  • @vertesians
  • @superdump
  • @wanderrful
  • @Neo-Zhixing
  • @rustui
  • @cart
  • @JohnTheCoolingFan
  • @pascualex
  • @fishykins
  • @Carlrs
  • @leath-dub

完整变更日志 #

添加了 #

更改 #

修复 #