Bevy 0.13

发布日期:2024 年 2 月 17 日,作者:Bevy 贡献者

感谢 **198** 位贡献者、**672** 次拉取请求、社区审阅者以及我们**慷慨的赞助商**,我们很高兴在 crates.io 上发布 **Bevy 0.13**!

对于那些不了解 Bevy 的人来说,Bevy 是一个用 Rust 编写的令人耳目一新、简单易用的数据驱动游戏引擎。您可以查看我们的快速入门指南,立即试用。它是永远免费的开源软件!您可以在 GitHub 上获取完整的源代码。查看 Bevy 资产,了解由社区开发的插件、游戏和学习资源的集合。要亲身体验引擎的功能,请查看最新 Bevy Jam 中的参赛作品,包括获胜者That's a LOT of beeeeees

要将现有的 Bevy 应用程序或插件更新到 **Bevy 0.13**,请查看我们的0.12 到 0.13 迁移指南

自从我们几个月前的上一个版本以来,我们添加了许多新功能、错误修复和生活质量调整,但以下是一些亮点

  • 光照贴图: 一种快速且流行的烘焙全局照明技术,适用于静态几何体(在 The Lightmapper 等程序中外部烘焙)。
  • 辐射体积 / 体素全局照明: 一种烘焙的全局照明形式,它在长方体内的体素中心对光进行采样(在 Blender 等程序中外部烘焙)。
  • 近似间接镜面遮挡: 通过减少镜面遮挡导致的镜面光泄漏,提高了照明的真实感。
  • 反射探头: 一种烘焙的轴对齐环境贴图形式,允许为静态几何体提供逼真的反射(在 Blender 等程序中外部烘焙)
  • 基本形状: 基本形状是游戏引擎和视频游戏的核心构建块:我们添加了一个经过精心设计、随时可用的集合!
  • 系统步进: 彻底暂停并逐帧或逐系统地推进游戏,以便以交互方式调试游戏逻辑,同时渲染继续更新。
  • 动态查询: 从系统内部细化查询具有极高的表达能力,并且是运行时定义类型以及第三方模组和脚本集成最后的关键部分。
  • 自动推断的命令刷新点: 不再需要考虑在哪里放置 apply_deferred 以及为什么命令没有被应用?我们也一样!现在,Bevy 的调度程序使用普通的 .before.after 约束,并检查系统参数以自动推断(并消除重复)同步点。
  • 切片、贴图和九宫格 2D 图像: 九宫格布局是用于平滑缩放风格化图集和 UI 的一种流行工具。现在在 Bevy 中可以使用了!
  • 相机驱动的 UI: UI 实体树现在可以被有选择地添加到任何相机,而不是全局应用到所有相机,从而实现诸如分屏 UI 等功能!
  • 相机曝光: 通过 EV100、f 值、快门速度和 ISO 感光度,对相机曝光进行逼真/“真实世界”控制。灯光也已调整,使其单位更逼真。
  • 动画插值模式: Bevy 现在支持导出 glTF 动画中的非线性插值模式。

初始烘焙照明 #

实时计算照明非常昂贵;但对于场景中永远不会移动的元素(如房间或地形),我们可以通过提前使用全局照明计算照明和阴影来获得更漂亮的照明和阴影,然后将结果存储在永远不会改变的“烘焙”形式中。全局照明是一种更逼真(且更昂贵)的照明方法,通常使用光线追踪。与 Bevy 的默认渲染不同,它考虑了光线从其他物体上反弹的情况,通过包含间接光来产生更逼真的效果。

光照贴图 #

作者:@pcwalton

lightmaps

光照贴图是存储预计算全局照明结果的纹理。几十年来,它们一直是实时图形的主流。Bevy 0.13 添加了对在其他程序(如 The Lightmapper)中计算的光照贴图进行渲染的初步支持。最终,我们希望在 Bevy 中直接添加对烘焙光照贴图的支持,但这一步打开了光照贴图工作流程!

正如lightmaps 示例所示,只需加载烘焙的光照贴图图像,然后在相应的网格上插入一个Lightmap 组件。

辐射体积 / 体素全局照明 #

作者:@pcwalton

irradiance volume

辐射体积(或体素全局照明)是一种通过首先将场景划分为立方体(体素),然后对每个体素的中心存在的光量进行采样,从而近似间接光的方法。然后,随着物体在该空间中移动,该光会被添加到这些物体上,从而适当地改变这些物体上的环境光级别。

我们选择使用基于 Half Life 2 的环境立方体算法。这使我们能够匹配 Blender 的 Eevee 渲染器,为用户提供一个简单且免费的路径来为他们自己的场景创建美观的辐射体积。

注意,由于辐射体积,这个球体在移动时会微妙地拾取环境的颜色

目前,您需要使用 Blender 等外部工具烘焙辐射体积,但在将来,我们希望在 Bevy 中直接支持烘焙辐射体积!

最小反射探头 #

作者:@pcwalton

环境贴图是 2D 纹理,用于在 3D 场景中模拟照明、反射和天空盒。反射探头将环境贴图推广,允许在同一个场景中使用多个环境贴图,每个环境贴图都有自己的轴对齐边界框。这是基于物理的渲染器的标准功能,灵感来自 Blender 的 Eevee 渲染器中的相应功能

反射探头 PR 中,我们添加了对它们的初步支持,为 Bevy 游戏中的漂亮、高性能反射奠定了基础。与上面讨论的烘焙全局照明工作一样,这些目前必须在外部预计算,然后导入到 Bevy 中。正如 PR 中所讨论的,存在一些注意事项:WebGL2 支持实际上不存在,由于没有混合,将观察到尖锐且突然的过渡,并且同一个世界中给定类型(漫射或镜面)的所有立方体贴图必须具有相同的尺寸、格式和mipmap 数量。

reflection probes

近似间接镜面遮挡 #

作者:@aevyrie

Bevy 当前的 PBR 渲染器过度亮化了图像,尤其是在掠射角,菲涅尔效应往往使表面像镜子一样。这种过度亮化是由于表面必须反射某些东西,但如果没有路径追踪或屏幕空间反射,渲染器必须猜测反射什么。它能做出的最佳猜测是采样环境立方体贴图,即使光线可能在到达环境光之前撞击了其他东西。这种忽略光遮挡的伪像被称为镜面光泄漏。

考虑一个汽车轮胎;虽然橡胶可能很光滑,但您不会期望它在轮毂井内有明亮的镜面高光,因为汽车本身正在阻挡(遮挡)会导致这些反射的光线。完全检查遮挡在计算上可能很昂贵。

Bevy 0.13 添加了对近似间接镜面遮挡的支持,它利用我们现有的 屏幕空间环境光遮挡近似镜面遮挡,可以在实时运行的同时产生相当高质量的结果。

拖动此图像进行比较

Specular Occlusion OnSpecular Occlusion Off
模型来源:BMW R1200GS 摩托车 由 Moto3D 创作,授权协议为 CC-BY-4.0

将来,这可以通过屏幕空间反射 (SSR) 进一步改进。但是,传统观点认为,您应该将镜面遮挡与 SSR 结合使用,因为 SSR 仍然存在光线泄漏伪像。

基本形状 #

作者:@Jondolf、@NiseVoid、@aevyrie

几何形状在整个游戏开发中被广泛使用,从基本网格形状和调试工具到物理碰撞体和射线投射。尽管在多个领域中被如此广泛地使用,但 Bevy 实际上并没有任何通用的形状表示。

这在Bevy 0.13中正在发生改变,引入了第一方基本形状!它们是轻量级的几何基元,旨在实现最大程度的互操作性和可重用性,允许 Bevy 和第三方插件使用相同的基本形状集,并增强生态系统内的凝聚力。有关更多详细信息,请参阅原始的 RFC

内置的 基元集合 已经相当庞大。

2D3D
矩形长方体
圆形球体
椭圆形
二维三角形
二维平面三维平面
二维线三维线
二维线段三维线段
Polyline2dBoxedPolyline2dPolyline3dBoxedPolyline3d
PolygonBoxedPolygon
正多边形
二维胶囊体三维胶囊体
圆柱体
圆锥体
圆锥台
环面

更多基元 将在将来的版本中添加。

基本形状的一些用例包括网格化、工具、包围体、碰撞体和射线投射功能。其中一些已经在 0.13 中实现!

渲染 #

基本形状可以使用网格和工具进行渲染。在本节中,我们将仔细研究新的 API。

下面,您可以看到使用网格和工具渲染的长方体和环面。您可以在新的 渲染基元 示例中查看所有可以渲染的基元。

On the left: A cuboid rendered with gizmos. It consists of 12 white lines. On the right: A cuboid rendered with meshes. It consists of 6 white faces.

On the left: A torus rendered with gizmos. It consists of many small rings, all connected by 4 big rings. On the right: A torus rendered with meshes. A shape that looks like a donut.

网格化 #

作者:@Jondolf

以前的 Bevy 版本包含诸如 QuadBoxUVSphere 之类的类型,用于从基本形状创建网格。这些类型已弃用,取而代之的是使用新的几何基元的构建器式 API。

支持网格化的基元实现了 Meshable 特性。对于某些形状,mesh 方法直接返回一个 Mesh

let before = Mesh::from(Quad::new(Vec2::new(2.0, 1.0)));
let after = Rectangle::new(2.0, 1.0).mesh(); // Mesh::from also works

但是,对于大多数基元,它返回一个用于可选配置的构建器。

// Create a circle mesh with a specified vertex count
let before = Mesh::from(Circle {
    radius: 1.0,
    vertices: 64,
});
let after = Circle::new(1.0).mesh().resolution(64).build();

以下是一些使用新的基元进行网格化的更多示例。

// Icosphere
let before = meshes.add(
    Mesh::try_from(Icosphere {
        radius: 2.0,
        subdivisions: 8,
    })
    .unwrap()
);
let after = meshes.add(Sphere::new(2.0).mesh().ico(8).unwrap());

// Cuboid
// (notice how Assets::add now also handles mesh conversion automatically)
let before = meshes.add(Mesh::from(shape::Box::new(2.0, 1.0, 1.0)));
let after = meshes.add(Cuboid::new(2.0, 1.0, 1.0));

// Plane
let before = meshes.add(Mesh::from(Plane::from_size(5.0)));
let after = meshes.add(Plane3d::default().mesh().size(5.0, 5.0));

随着基元的添加,网格化也支持更多形状,例如 EllipseTriangle2dCapsule2d。但是,请注意,网格化尚未在所有基元中实现,例如 PolygonCone

您可以在下面的 2d_shapes3d_shapes 示例中看到一些网格。

An example with 2D mesh shapes

An example with 3D mesh shapes

网格形状尺寸的一些默认值也已更改,以实现更高的一致性。

工具 #

作者:@RobWalt

基元也可以使用 Gizmos 进行渲染。有两个新的泛型方法

某些基元可能具有类似于现有 Gizmos 绘制方法的附加配置选项。例如,使用 Sphere 调用 primitive_3d 会返回一个 SphereBuilder,它提供了一个 segments 方法来控制球体的细节级别。

let sphere = Sphere { radius };
gizmos
    .primitive_3d(sphere, center, rotation, color)
    .segments(segments);

包围体 #

作者:@NiseVoid、@Jondolf

在游戏开发中,空间检查具有许多宝贵的用例,例如获取在摄像机视锥体中或靠近玩家的所有实体,或查找可能发生碰撞的物理对象对。为了加快这些检查,包围体用于逼近更复杂的形状。

Bevy 0.13 添加了一些新的公开可用的包围体:Aabb2dAabb3dBoundingCircleBoundingSphere。这些可以手动创建,也可以从基元形状生成。

每个包围体都实现了 BoundingVolume 特性,提供了一些通用功能和帮助程序。可以使用 IntersectsVolume 特性来测试与这些包围体的交点。此特性在包围体本身中实现,因此您可以测试它们之间的交点。这在所有现有的包围体类型之间都支持,但仅支持同一维度的那些类型。

以下是如何构造包围体以及如何执行交点测试的示例

// We create an axis-aligned bounding box that is centered at position
let position = Vec2::new(100., 50.);
let half_size = Vec2::splat(20.);
let aabb = Aabb2d::new(position, half_size);

// We create a bounding circle that is centered at position
let position = Vec2::new(80., 70.);
let radius = 30.;
let bounding_circle = BoundingCircle::new(position, radius);

// We check if the volumes are intersecting
let intersects = bounding_circle.intersects(&aabb);

还有两个用于生成包围体的特性:Bounded2dBounded3d。这些在新的基元形状中实现,因此您可以轻松地为它们计算包围体。

// We create a primitive, a hexagon in this case
let hexagon = RegularPolygon::new(50., 6);

let translation = Vec2::new(50., 200.);
let rotation = PI / 2.; // Rotation in radians

// Now we can get an Aabb2d or BoundingCircle from this primitive.
// These methods are part of the Bounded2d trait.
let aabb = hexagon.aabb_2d(translation, rotation);
let circle = hexagon.bounding_circle(translation, rotation);

射线投射和体积投射 #

包围体还支持基本的射线投射和体积投射。射线投射测试一个包围体是否与给定射线相交,该射线从一个原点沿一个方向投射,直到最大距离。体积投射的工作原理类似,但就像沿着射线移动一个体积一样。

此功能通过新的 RayCast2dRayCast3dAabbCast2dAabbCast3dBoundingCircleCastBoundingSphereCast 类型提供。它们可用于检查与包围体的交点,以及计算从投射原点到交点的距离。

下面,您可以看到射线投射、体积投射和交点测试在实际中的应用

为了更容易地理解不同维度中的射线投射,旧的 Ray 类型也已拆分为 Ray2dRay3d。新的 Direction2dDirection3d 类型用于确保射线方向保持归一化,从而提供类型级保证,即向量始终为单位长度。这些类型已经在其他一些 API 中使用,例如某些基元和工具方法。

系统步进 #

作者:@dmlary

Bevy 0.13 添加了对系统步进的支持,这为系统添加了类似于调试器的执行控制。

可以使用 Stepping 资源来控制计划中每帧执行哪些系统,并提供步进、断点和继续功能以启用实时调试。

let mut stepping = Stepping::new();

您将要逐步执行的计划添加到 Stepping 资源中。这些计划中的系统可以被认为是“步进帧”。“步进帧”中的系统除非发生相关的步进或继续操作,否则不会运行。未添加的计划将在每次更新时运行,即使在步进过程中也是如此。这使核心功能(如渲染)能够继续工作。

stepping.add_schedule(Update);
stepping.add_schedule(FixedUpdate);

即使插入了资源,步进默认情况下也是禁用的。要在应用程序中启用它,功能标志、开发控制台和模糊的热键都可以很好地工作。

#[cfg(feature = "my_stepping_flag")]
stepping.enable();

最后,将 Stepping 资源添加到 ECS World 中。

app.insert_resource(stepping);

系统步进和继续帧 #

“步进帧”操作会运行步进光标处的系统,并在下一渲染帧中向前移动光标。这对于查看系统所做的单个更改以及在执行系统之前查看世界的状态非常有用。

stepping.step_frame()

“继续帧”操作将在下一帧中从步进光标开始执行系统,直到步进帧结束。如果遇到带有断点的系统,它可能会在步进帧结束之前停止。这对于快速遍历整个帧、到达下一帧的开头或与断点结合使用非常有用。

stepping.continue_frame()

这段视频演示了这些操作在 breakout 示例中的应用,该示例使用自定义的 egui 接口。当我们点击 step 按钮时,可以看到步进光标在系统列表中移动。当点击 continue 按钮时,可以看到游戏每次点击都前进一个步进帧。

断点 #

当计划发展到一定程度时,可能需要花费很长时间才能遍历计划中的每个系统,只是为了查看几个系统的效果。在这种情况下,步进提供系统断点。

这段视频说明了 check_for_collisions() 上的断点在“步进”和“继续”操作中的行为。

在步进期间禁用系统 #

在调试期间,禁用系统以缩小问题的来源可能会有所帮助。可以使用 Stepping::never_run()Stepping::never_run_node() 在启用步进时禁用系统。

将系统从步进中排除 #

可能需要确保某些系统在启用步进时仍然运行。虽然最佳实践是将它们放在一个未添加到 Stepping 资源的计划中,但可以将系统配置为在启用步进时始终运行。这主要对于事件和输入处理系统有用。

可以通过调用 `Stepping::always_run()` 或 `Stepping::always_run_node()` 将系统配置为始终运行。当系统配置为始终运行时,即使启用了步进,它也会在每个渲染帧运行。

局限性 #

步进的初始实现有一些局限性。

  • **读取事件的系统可能无法正常步进:**因为在启用步进时帧仍然正常前进,所以在步进系统读取事件之前,事件可能会被清除。这里最好的方法是将基于事件的系统配置为始终运行,或者将它们放到未添加到 `Stepping` 的调度中。在这种情况下,“继续”带断点可能也有效。
  • **条件系统在步进时可能无法按预期运行:**与基于事件的系统类似,如果运行条件仅在短时间内为真,则系统在步进时可能不会运行。

详细示例 #

相机曝光 #

作者:@superdump (Rob Swain), @JMS55, @cart

在现实世界中,相机捕获的图像亮度由曝光量决定:相机传感器或胶片吸收的光量。这由相机的几种机制控制。

  • **光圈:**以 F 档表示,光圈打开和关闭以控制允许进入相机传感器或胶片的光量,通过物理方式阻挡来自特定角度的光线,类似于眼睛的瞳孔。
  • **快门速度:**相机快门打开的时间,即相机传感器或胶片曝光的光线持续时间。
  • **ISO 感光度:**相机传感器或胶片对光的敏感度。值越高表示对光的敏感度越高。

这些因素都会影响最终图像接收的光量。它们可以组合成最终的 EV 值(曝光值),例如半标准 EV100(ISO 100 的曝光值)。更高的 EV100 值意味着需要更多的光线才能获得相同的结果。例如,阳光明媚的场景可能需要约 15 的 EV100,而光线昏暗的室内场景可能需要约 7 的 EV100。

在 **Bevy 0.13** 中,您现在可以使用新的 Exposure 组件按相机配置 EV100。您可以使用 Exposure::ev100 字段直接设置它,或者使用新的 PhysicalCameraParameters 结构使用“现实世界”相机设置(例如 f 档、快门速度和 ISO 感光度)计算 EV100。

这很重要,因为 Bevy 的“基于物理”渲染器(PBR)有意扎根于现实。我们的目标是让人们能够在他们的灯光和材质中使用现实世界单位,并使它们的行为尽可能接近现实。

拖动此图像进行比较

EV100 9.7EV100 15

请注意,Bevy 的早期版本为其某些灯光类型硬编码了静态 EV100。在 **Bevy 0.13** 中,它是可配置的,并且在所有灯光类型中保持一致。我们还将默认 EV100 提升到 9.7,这是一个 我们选择与 Blender 的默认曝光最匹配的数字。它恰好也是一个不错的“中间值”,位于室内照明和阴天室外照明之间。

您可能会注意到,点光源现在需要 *显著* 更高的强度值(以流明表示)。这种(有时)百万流明的值可能感觉过高。请放心,(1)在阴天室外环境中,实际上需要大量的灯光才能产生有意义的照明,(2)Blender 以这种规模导出灯光(而我们校准得尽可能接近它们)。

相机驱动 UI #

作者:@bardt, @oceantume

从历史上看,Bevy 的 UI 元素在主窗口的上下文中进行缩放和定位,与相机设置无关。这种方法使一些 UI 体验(例如分屏多人游戏)难以实现,而另一些(例如在多个窗口中使用 UI)则不可能实现。

**Bevy 0.13** 引入了 **相机驱动 UI**。每个相机现在都可以拥有自己的 UI 根节点,根据其视口、缩放因子和目标进行渲染,目标可以是辅助窗口,甚至可以是纹理。

此更改解锁了各种新的 UI 体验,包括分屏多人游戏、多个窗口中的 UI、在 3D 世界中显示非交互式 UI 等等。

Split-screen with independent UI roots

如果世界上只有一个相机,您无需进行任何操作;您的 UI 将显示在该相机的视口中。

commands.spawn(Camera3dBundle {
    // Camera can have custom viewport, target, etc.
});
commands.spawn(NodeBundle {
    // UI will be rendered to the singular camera's viewport
});

当需要更多控制,或存在多个相机时,我们引入了 TargetCamera 组件。此组件可以添加到根 UI 节点以指定应将其渲染到哪个相机。

// For split-screen multiplayer, we set up 2 cameras and 2 UI roots
let left_camera = commands.spawn(Camera3dBundle {
    // Viewport is set to left half of the screen
}).id();

commands
    .spawn((
        TargetCamera(left_camera),
        NodeBundle {
            //...
        }
    ));

let right_camera = commands.spawn(Camera3dBundle {
    // Viewport is set to right half of the screen
}).id();

commands
    .spawn((
        TargetCamera(right_camera),
        NodeBundle {
            //...
        })
    );

通过此更改,我们还删除了 UiCameraConfig 组件。如果您正在使用它来隐藏 UI 节点,则可以通过在根节点上配置 Visibility 组件来实现相同的效果。

commands.spawn(Camera3dBundle::default());
commands.spawn(NodeBundle {
    visibility: Visibility::Hidden, // UI will be hidden
    // ...
});

纹理切片和贴图 #

作者:@ManevilleF

3D 渲染得到了很多关注,但 2D 功能也很重要!我们很高兴在 **Bevy 0.13** 中为 bevy_spritebevy_ui 添加基于 CPU 的 *切片和贴图*!

此行为由一个新的可选组件控制:ImageScaleMode

9 切片 #

在具有精灵或 UI 捆绑的实体上添加 ImageScaleMode::Sliced 会启用 9 切片,在调整大小期间保持图像比例,避免纹理拉伸。

Stretched Vs Sliced texture

这对于 UI 非常有用,允许您的漂亮纹理在元素大小改变时保持正确的显示。

Sliced Buttons

Kenney 提供的边框纹理
commands.spawn((
    SpriteSheetBundle::default(),
    ImageScaleMode::Sliced(TextureSlicer {
        // The image borders are 20 pixels in every direction
        border: BorderRect::square(20.0),
        // we don't stretch the corners more than their actual size (20px)
        max_corner_scale: 1.0,
        ..default()
    }),
));

贴图 #

在您的 2D 精灵实体上添加 ImageMode::Tiled { .. } 会启用 *纹理贴图*:重复图像,直到它们的整个区域都被填充。这通常用于背景和表面。

commands.spawn((
    SpriteSheetBundle::default(),
    ImageScaleMode::Tiled {
        // The image will repeat horizontally
        tile_x: true,
        // The image will repeat vertically
        tile_y: true,
        // The texture will repeat if the drawing rect is larger than the image size
        stretch_value: 1.0,
    },
));

动态查询 #

作者:@james-j-obrien, @jakobhellermann, @Suficio

在 Bevy ECS 中,查询使用类型驱动的 DSL。查询的完整类型(要访问的组件、要使用的过滤器)必须在编译时指定。

有时我们无法在编译时知道查询要访问哪些数据。有些场景根本无法使用静态查询来完成。

  • 在 Lua 或 JavaScript 等脚本语言中定义查询。
  • 从脚本语言中定义新的组件并查询它们。
  • 向实体检查器(如 bevy-inspector-egui)添加运行时过滤器。
  • Quake 风格的控制台 添加功能,以便从提示符在运行时修改或查询组件。
  • 创建具有远程功能的 编辑器

动态查询使所有这些成为可能。而这些只是我们迄今为止听到的计划!

定义 Query 的标准方法是将它们用作系统参数。

fn take_damage(mut player_health: Query<(Entity, &mut Health), With<Player>>) {
    // ...
}

**这不会改变。**对于大多数(如果不是全部)游戏玩法用例,您将继续愉快地使用令人愉快的简单 Query API。

但是,请考虑这种情况:作为游戏或模组开发者,我想通过文本提示列出具有特定组件的实体。类似 Quake 控制台的工作方式。这将是什么样子呢?

#[derive(Resource)]
struct UserQuery(String);

// user_query is entered as a text prompt by the user when the game is running.
// In a system, it's quickly apparent that we can't use `Query`.
fn list_entities_system(user_query: Res<UserQuery>, query: Query<FIXME, With<FIXME>>) {}

// Even when using the more advanced `World` API, we are stuck.
fn list_entities(user_query: String, world: &mut World) {
    // FIXME: what type goes here?
    let query = world.query::<FIXME>();
}

根据 `user_query` 的值选择类型是不可能的!QueryBuilder 解决了这个问题。

fn list_entities(
    user_query: String,
    type_registry: &TypeRegistry,
    world: &mut World,
) -> Option<()> {
    let name = user_query.split(' ').next()?;
    let type_id = type_registry.get_with_short_type_path(name)?.type_id();
    let component_id = world.components().get_id(type_id)?;

    let query = QueryBuilder::<FilteredEntityRef>::new(&mut world)
        .ref_id(component_id)
        .build();

    for entity_ref in query.iter(world) {
        let ptr = entity_ref.get_by_id(component_id);
        // Convert `ptr` into a `&dyn Reflect` and use it.
    }
    Some(())
}

它仍然是一个容易出错、复杂且不安全的 API,但它使以前不可能的事情成为可能。我们希望第三方 crate 为 `QueryBuilder` API 提供便捷的包装器,其中一些包装器无疑会进入上游。

查询转化 #

作者:@hymm, james-j-obrien

您是否曾经想将查询传递给函数,但不是拥有 Query<&Transform>,而是拥有 Query<(&Transform, &Velocity), With<Enemy>>?在 **Bevy 0.13** 中,您可以做到这一点,这得益于新的 QueryLensQuery::transmute_lens() 方法。

查询转化允许您将查询更改为不同的查询类型,只要访问的组件是原始查询的子集。如果您尝试访问不在原始查询中的数据,此方法将出现恐慌。

fn reusable_function(lens: &mut QueryLens<&Transform>) {
    let query = lens.query();
    // do something with the query...
}

// We can use the function in a system that takes the exact query.
fn system_1(mut query: Query<&Transform>) {
    reusable_function(&mut query.as_query_lens());
}

// We can also use it with a query that does not match exactly
// by transmuting it.
fn system_2(mut query: Query<(&mut Transform, &Velocity), With<Enemy>>) {
    let mut lens = query.transmute_lens::<&Transform>();
    reusable_function(&mut lens);
}

请注意,QueryLens 仍然会遍历与它派生自的原始 Query 相同的实体。从 Query<(&Transform, &Velocity)> 中获取的 QueryLens<&Transform> 仅包含具有 `Transform` 和 `Velocity` 组件的实体的 `Transform`。

除了删除参数,您还可以以有限的方式将它们更改为不同的智能指针类型。其中最有用的是将 &mut 更改为 &。有关更多详细信息,请参阅 文档

需要注意的是,转化并非免费的。它通过创建一个新状态并在原始查询中复制缓存数据来工作。这不是一项昂贵的操作,但您应该避免在热循环中进行。

WorldQuery 特性拆分 #

作者:@wainwrightmark @taizu-jin

Query 具有两个类型参数:一个用于要获取的数据,第二个可选参数用于过滤器。

在 Bevy 的早期版本中,这两个参数都只需要 WorldQuery:没有任何东西阻止您在数据位置(反之亦然)中使用作为过滤器的类型。

除了使 Query 项目的类型签名更复杂(见下例)之外,这通常效果很好,因为大多数过滤器在任一位置的行为相同。

不幸的是,情况并非如此 ChangedAdded 它们在数据位置有不同的(未记录的)行为,这会导致用户代码中的错误。

为了让我们能够在编译时阻止这种类型的错误,WorldQuery 特性已被两个特性替换:QueryDataQueryFilter。现在,Query 的数据参数必须是 QueryData,过滤器参数必须是 QueryFilter

大多数用户代码应该不受影响或易于迁移。

// Probably a subtle bug: `With` filter in the data position - will not compile in 0.13
fn my_system(query: Query<(Entity, With<ComponentA>)>)
{
    // The type signature of the query items is `(Entity, ())`, which is usable but unwieldy
  for (entity, _) in query.iter(){
  }
}

// Idiomatic, compiles in both 0.12 and 0.13
fn my_system(query: Query<Entity, With<ComponentA>>)
{
  for entity in query.iter(){
  }
}

自动插入 `apply_deferred` 系统 #

作者:@hymm

在编写游戏玩法代码时,您通常会遇到一个系统希望立即看到另一个系统中排队的命令的效果。在**Bevy 0.13**之前,您需要在两个系统之间手动插入一个`apply_deferred`系统,这是一个特殊系统,会在遇到这些命令时应用它们。现在,Bevy 会检测带有命令的系统相对于其他系统的排序情况,并为您插入`apply_deferred`系统。

// Before 0.13
app.add_systems(
    Update,
    (
        system_with_commands,
        apply_deferred,
        another_system,
    ).chain()
);
// After 0.13
app.add_systems(
    Update,
    (
        system_with_commands,
        another_system,
    ).chain()
);

这解决了常见的初学者错误:如果两个系统是按顺序排列的,那么第二个系统不应该总是看到第一个系统的结果吗?

自动插入的`apply_deferred`系统通过自动合并它们(如果可能的话)来进行优化。在大多数情况下,建议删除所有手动插入的`apply_deferred`系统,因为允许 Bevy 按需插入和合并这些系统通常会更快,并且涉及更少的样板代码。

// This will only add one apply_deferred system.
app.add_systems(
    Update,
    (
        (system_1_with_commands, system_2).chain(),
        (system_3_with_commands, system_4).chain(),
    )
);

如果这种新行为不适合您,请查阅迁移指南。有几个新 API 允许您选择退出。

更灵活的一次性系统 #

作者:@Nathan-Fenner

在**Bevy 0.12**中,我们引入了一次性系统,这是一种方便的方法,可以在需要时调用系统,而无需将它们添加到调度程序中。最初的实现对哪些系统可以和不能用作一次性系统有一些限制。在**Bevy 0.13**中,这些限制已得到解决。

一次性系统现在支持输入和输出。

fn increment_sys(In(increment_by): In<i32>, mut counter: ResMut<Counter>) -> i32 {
    counter.0 += increment_by;
    counter.0
}

let mut world = World::new();
let id = world.register_system(increment_sys);

world.insert_resource(Counter(1));
let count_one = world.run_system_with_input(id, 5).unwrap(); // increment counter by 5 and return 6
let count_two = world.run_system_with_input(id, 2).unwrap(); // increment counter by 2 and return 8

运行系统现在将系统输出作为`Ok(output)`返回。请注意,通过命令调用一次性系统时,由于其延迟性质,无法返回输出。

独占系统现在可以注册为一次性系统。

world.register_system(|world: &mut World| { /* do anything */ });

装箱系统现在可以使用`register_boxed_system`注册。

这些改进极大地完善了一次性系统:它们现在应该与其他任何 Bevy 系统一样工作。

wgpu 0.19 升级和渲染性能改进 #

作者:@Elabajaba、@JMS55

在**Bevy 0.13**中,我们从`wgpu` 0.17 升级到`wgpu` 0.19,其中包括期待已久的`wgpu` arcanization,它允许我们异步编译着色器以避免着色器编译卡顿,以及多线程绘制调用创建,以在 CPU 受限场景中获得更好的性能。

由于 wgpu 0.19 中的更改,我们在 Bevy 中添加了一个新的`webgpu`特性,现在在针对 WebGPU 进行 WebAssembly 构建时需要该特性。在针对 WebGPU 时,不再需要禁用`webgl2`特性,但新的`webgpu`特性在启用时当前会覆盖`webgl2`特性。库作者,请不要默认启用`webgpu`特性。将来,我们计划允许您在同一个 WebAssembly 二进制文件中同时针对 WebGL2 和 WebGPU,但我们还没有完全实现这一目标。

我们交换了材质和网格绑定组,因此网格数据现在位于绑定组 1 中,而材质数据位于绑定组 2 中。这与更改不透明通道的排序函数以按管道和网格排序相结合,极大地改善了我们的绘制调用批处理。以前,我们按距离相机排序。这些批处理改进意味着我们执行的绘制调用更少,从而提高了 CPU 性能,尤其是在较大的场景中。我们还删除了着色器中的`get_instance_index`函数,因为它仅用于解决 wgpu 0.19 中已修复的上游错误。有关其他着色器或渲染更改,请参阅迁移指南wgpu 的更改日志

对 Bevy 和`wgpu`的许多细微更改加起来在真实的 3D 场景中产生了适度但可衡量的性能差异!我们在同一台机器上对四个复杂场景(BistroSponzaSan MiguelHidden Alley)运行了一些快速测试,分别在**Bevy 0.12**和**Bevy 0.13**上。

A high polygon, realistically lit screenshot of a beautiful cafe with a tree in the background.

如您所见,这些场景比大多数视频游戏环境详细得多,但该屏幕截图是在 Bevy 中以 1440p 分辨率以超过 60 FPS 的速度渲染的!在 Bevy 0.12 和 Bevy 0.13 之间,我们看到测试场景的帧时间减少了约 5-10%。干得好!

A graph showing the FPS of the different scenes. Bistro went from 90 FPS to 120 FPS, while the other scenes improved slightly. All scenes were 60-120 FPS.

从 RAM 中卸载渲染资产 #

作者:@JMS55、@mockersf、@brianreavis

网格和用于定义其材质的纹理占用大量的内存:在许多游戏中,内存使用是游戏分辨率和多边形数量的最大限制!此外,将这些数据从系统 RAM(CPU 使用)传输到 VRAM(GPU 使用)可能会成为真正的性能瓶颈。

**Bevy 0.13** 添加了从系统 RAM 中卸载这些数据的功能,一旦它们已成功传输到 VRAM。要为您的资产配置此行为,请设置RenderAssetUsages 字段,以指定是在主(CPU)世界、渲染(GPU)世界还是两者中保留数据。

这种行为目前在大多数资产类型中默认处于关闭状态,因为它存在一些注意事项(因为资产变得对 CPU 上的逻辑不可用),但我们强烈建议您尽可能为您的资产启用它,以获得显著的内存使用量提升(我们可能会在将来将其默认启用)。

纹理图集和字体图集现在只提取实际使用的部分数据到 VRAM,而不是浪费工作量,每帧将所有可能的图像或字符发送到 VRAM。真棒!

通过更智能的排序获得更好的批处理 #

作者:@Elabajaba

用于加速渲染的核心技术之一是同时绘制许多相似的对象。在这种情况下,Bevy 已经使用了一种称为“批处理”的技术,它允许我们合并多个相似的操作,减少正在进行的昂贵的绘制调用(对 GPU 的指令)数量。

但是,我们定义这些批次的策略远非最佳。以前,我们按距离相机排序,然后检查该排序列表中是否有多个相同的网格彼此相邻。在真实的场景中,这不太可能找到许多合并候选者!

在**Bevy 0.13**中,我们首先按管道(实际上是使用的材质类型)排序,然后按网格标识排序。这种策略会导致更好的批处理,在测试的真实场景中将整体 FPS 提高了两位数的百分比!

A graph showing batching improvements. Shadows are very expensive, and FPS improved by at least 20% in all cases tested.

动画插值方法 #

作者:@mockersf

通常,动画由它们的关键帧定义:在时间轴上某个时刻的对象位置(和其他状态)的快照。但是,这些关键帧之间发生了什么?游戏引擎需要在它们之间进行插值,平滑地从一个状态过渡到另一个状态。

最简单的插值方法是线性:动画对象每单位时间都向下一个关键帧移动相同距离。但这并不总是我们想要的效果!定格动画风格和更平滑的动画都有其用武之地。

Bevy 现在支持动画中的分步和三次样条插值。大多数情况下,这将直接从 glTF 文件中正确解析,但在手动设置VariableCurve 时,有一个新的Interpolation 字段需要设置。

Demonstrating the different types of interpolation

Animatable 特性 #

作者:@james7132

当您想到“动画”时:您可能想到的是在空间中移动对象。将它们来回平移,旋转它们,甚至可能挤压和拉伸它们。但在现代游戏开发中,动画是一组功能强大的共享工具和概念,用于“随时间改变事物”。变换只是开始:颜色、粒子效果、不透明度,甚至诸如可见性之类的布尔值都可以进行动画处理!

在**Bevy 0.13**中,我们迈出了这一愿景的第一步,使用了Animatable 特性。

/// An animatable value type.
pub trait Animatable: Reflect + Sized + Send + Sync + 'static {
    /// Interpolates between `a` and `b` with  a interpolation factor of `time`.
    ///
    /// The `time` parameter here may not be clamped to the range `[0.0, 1.0]`.
    fn interpolate(a: &Self, b: &Self, time: f32) -> Self;

    /// Blends one or more values together.
    ///
    /// Implementors should return a default value when no inputs are provided here.
    fn blend(inputs: impl Iterator<Item = BlendInput<Self>>) -> Self;

    /// Post-processes the value using resources in the [`World`].
    /// Most animatable types do not need to implement this.
    fn post_process(&mut self, _world: &World) {}
}

这是朝着动画混合和资产驱动动画图迈出的第一步,这对于在 Bevy 中发布大型 3D 游戏至关重要。但就目前而言,这只是一个构建块。我们已经为一些关键类型(Transformf32 和 `glam` 的 `Vec` 类型)实现了这一点,并发布了该特性。将其插入到您的游戏和板条箱中,并与其他贡献者合作,帮助`bevy_animation`变得与引擎的其余部分一样令人愉快和功能丰富。

无扩展名资产支持 #

作者:@bushrat011899

在 Bevy 的早期版本中,为特定资产选择AssetLoader 的默认方法完全基于文件扩展名。最近添加了 .meta 文件,允许指定更细粒度的加载行为,但仍然需要文件扩展名。在**Bevy 0.13**中,现在可以使用资产类型推断AssetLoader

// Uses AudioSettingsAssetLoader
let audio = asset_server.load("data/audio.json");

// Uses GraphicsSettingsAssetLoader
let graphics = asset_server.load("data/graphics.json");

这是因为每个AssetLoader 都需要声明它加载的资产类型,而不仅仅是它支持的扩展名。由于load 方法在AssetServer 上已经对要返回的资产类型进行了泛化,因此该信息已经可用于AssetServer

// The above example with types shown
let audio: Handle<AudioSettings> = asset_server.load::<AudioSettings>("data/audio.json");
let graphics: Handle<GraphicsSettings> = asset_server.load::<GraphicsSettings>("data/graphics.json");

现在,我们也可以用它来选择AssetLoader 本身。

加载资产时,加载器将通过按顺序检查以下内容来选择:

  1. 资产`meta` 文件
  2. 要返回的`Handle<A>` 类型
  3. 文件扩展名
// This will be inferred from context to be a glTF asset, ignoring the file extension
let gltf_handle = asset_server.load("models/cube/cube.gltf");

// This still relies on file extension due to the label
let cube_handle = asset_server.load("models/cube/cube.gltf#Mesh0/Primitive0");
//                                                        ^^^^^^^^^^^^^^^^^
//                                                        | Asset path label

文件扩展名现在是可选的 #

由于可以使用资产类型推断加载器,因此要加载的文件或AssetLoader 都不需要文件扩展名。

pub trait AssetLoader: Send + Sync + 'static {
    /* snip */

    /// Returns a list of extensions supported by this [`AssetLoader`], without the preceding dot.
    fn extensions(&self) -> &[&str] {
        // A default implementation is now provided
        &[]
    }
}

以前,没有扩展名的资产加载器很难使用。现在,它们可以使用起来与其他任何加载器一样容易。同样,如果文件缺少其扩展名,Bevy 现在可以选择适当的加载器。

let license = asset_server.load::<Text>("LICENSE");

仍然建议使用适当的文件扩展名来进行良好的项目管理,但这现在只是一个建议,而不是硬性要求。

具有相同资产的多个资产加载器 #

现在,只要它们是不同的资产类型,单个路径就可以被多个资产句柄使用。

// Load the sound effect for playback
let bang = asset_server.load::<AudioSource>("sound/bang.ogg");

// Load the raw bytes of the same sound effect (e.g, to send over the network)
let bang_blob = asset_server.load::<Blob>("sound/bang.ogg");

// Returns the bang handle since it was already loaded
let bang_again = asset_server.load::<AudioSource>("sound/bang.ogg");

请注意,上面的示例使用了turbofish 语法以提高清晰度。在实践中,这不是必需的,因为加载的资产类型通常可以在调用点推断出来。

#[derive(Resource)]
struct SoundEffects {
    bang: Handle<AudioSource>,
    bang_blob: Handle<Blob>,
}

fn setup(mut effects: ResMut<SoundEffects>, asset_server: Res<AssetServer>) {
    effects.bang = asset_server.load("sound/bang.ogg");
    effects.bang_blob = asset_server.load("sound/bang.ogg");
}

已经更新了custom_asset 示例,以演示这些新功能。

纹理图集重做 #

作者:@ManevilleF

纹理图集有效地将多个图像组合到一个名为图集的更大的纹理中。

**Bevy 0.13** 对它们进行了重大重做,以减少样板代码并使它们更面向数据。告别`TextureAtlasSprite` 和`UiTextureAtlasImage` 组件(以及它们相应的`Bundle` 类型)。现在通过向普通精灵和图像实体添加一个附加组件来启用纹理图集:TextureAtlas

为什么? #

纹理图集(有时称为精灵表)只是绘制给定纹理的自定义部分。这仍然是精灵或图像行为,我们只是绘制了一个子集。新的 TextureAtlas 组件通过存储来体现这一点

  • 一个 Handle<TextureAtlasLayout>,一个将索引映射到纹理的 Rect 部分的资产
  • 一个 usize 索引,用于定义我们要显示的布局的哪个 Rect 部分

光照 RenderLayers #

作者:@robftm

RenderLayers 是 Bevy 对通过过滤相机所能看到的内容来快速隐藏和显示大量实体的答案……非常适合自定义角色手中持有物品的第一人称视角(或确保吸血鬼不会出现在你的镜子里!)。

RenderLayers 现在与灯光配合良好,修复了一个严重的限制,以确保这个很棒的功能能够适当发挥作用!

绑定组布局项 #

作者:@IceSentry

我们添加了一个新的 API,其灵感来自 0.12 中的绑定组项 API,用于声明绑定组布局。这个新的 API 基于使用内置函数来定义绑定组布局资源并根据其位置自动设置索引。

以下是一个关于如何声明新布局的简短示例

let layout = render_device.create_bind_group_layout(
    "post_process_bind_group_layout",
    &BindGroupLayoutEntries::sequential(
        ShaderStages::FRAGMENT,
        (
            texture_2d_f32(),
            sampler(SamplerBindingType::Filtering),
            uniform_buffer::<PostProcessingSettings>(false),
        ),
    ),
);

RenderGraph 的类型安全标签 #

作者:@DasLixou

Bevy 在定义标签时广泛使用 Rust 的类型系统,让开发人员可以依靠工具来捕获拼写错误并简化重构。但这不适用于 Bevy 的渲染图。在渲染图中,使用硬编码(并且可能重叠)的字符串来定义节点和子图。

// Before 0.13
impl MyRenderNode {
    pub const NAME: &'static str = "my_render_node"
}

在 **Bevy 0.13** 中,我们使用一种更健壮的方式来命名渲染节点和渲染图,借助于 bevy_ecs 已经使用的类型安全标签模式。

// After 0.13
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub struct PrettyFeature;

有了这些,常量值的较长路径将变得更短和更简洁

// Before 0.13
render_app
    .add_render_graph_node::<ViewNodeRunner<PrettyFeatureNode>>(
        core_3d::graph::NAME,
        PrettyFeatureNode::NAME,
    )
    .add_render_graph_edges(
        core_3d::graph::NAME,
        &[
            core_3d::graph::node::TONEMAPPING,
            PrettyFeatureNode::NAME,
            core_3d::graph::node::END_MAIN_PASS_POST_PROCESSING,
        ],
    );

// After 0.13
use bevy::core_pipeline::core_3d::graph::{Node3d, Core3d};

render_app
    .add_render_graph_node::<ViewNodeRunner<PrettyFeatureNode>>(
        Core3d,
        PrettyFeature,
    )
    .add_render_graph_edges(
        Core3d,
        (
            Node3d::Tonemapping,
            PrettyFeature,
            Node3d::EndMainPassPostProcessing,
        ),
    );

当您需要渲染节点的动态标签时,仍然可以通过例如元组结构来实现这些标签

#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub struct MyDynamicLabel(&'static str);

这特别好,因为我们不必在这里存储字符串:我们可以使用整数、自定义枚举或任何其他可散列类型。

Winit 升级 #

作者:@Thierry Berger, @mockersf

通过我们贡献者和审阅者的英勇努力,Bevy 现已升级 为使用 winit 0.29winit 是我们的窗口库:它抽象化了最终用户可能拥有的所有不同操作系统和输入设备,并提供了一个统一的 API 来实现一次编写,随处运行的体验。虽然这带来了通常的许多有价值的 错误修复和稳定性改进,但关键变化与如何处理 KeyCode 有关。

以前,KeyCode 代表键盘上一个键的逻辑意义:在 QWERTY 和 AZERTY 键盘布局之间切换时,在同一个键盘上按下同一个按钮会产生不同的结果!现在,KeyCode 代表按键的物理位置。WASD 游戏爱好者知道,这对于游戏来说是一个更好的默认值。对于大多数 Bevy 开发者来说,您可以保持现有代码不变,只需享受非 QWERTY 键盘或布局的用户更好的默认键绑定即可。如果您需要有关按下的逻辑键的信息,请使用 ReceivedCharacter 事件。

多个 Gizmo 配置 #

作者:@jeliag

Gizmos 允许您使用即时模式 API 快速绘制形状。以下是使用方法

// Bevy 0.12.1
fn set_gizmo_width(mut config: ResMut<GizmoConfig>) {
    // set the line width of every gizmos with this global configuration resource.
    config.line_width = 5.0;
}

fn draw_circles(mut gizmos: Gizmos) {
    // Draw two circles with a 5 pixels outline
    gizmos.circle_2d(vec2(100., 0.), 120., Color::NAVY);
    gizmos.circle_2d(vec2(-100., 0.), 120., Color::ORANGE);
}

添加一个 Gizmos 系统参数,只需调用几个方法。很酷!

Gizmos 也非常适合板条箱作者,他们可以使用相同的 API。例如,oxidized_navigation 导航网格库使用 gizmos 来进行其调试覆盖。很不错!

但是,只有一个全局配置。因此,依赖项很可能会影响游戏的 gizmos。它甚至可能使它们完全无法使用。

不太好。如何解决?Gizmo 组。

现在,Gizmos 带有一个可选参数。默认情况下,它使用全局配置

fn draw_circles(mut default_gizmos: Gizmos) {
    default_gizmos.circle_2d(vec2(100., 0.), 120., Color::NAVY);
}

但是使用 GizmoConfigGroup 参数,Gizmos 可以选择不同的配置

fn draw_circles(
    mut default_gizmos: Gizmos,
    // this uses the distinct NavigationGroup config
    mut navigation_gizmos: Gizmos<NavigationGroup>,
) {
    // Two circles with different outline width
    default_gizmos.circle_2d(vec2(100., 0.), 120., Color::NAVY);
    navigation_gizmos.circle_2d(vec2(-100., 0.), 120., Color::ORANGE);
}

通过派生 GizmoConfigGroup 并将其注册到 App 中来创建您自己的 gizmo 配置组

#[derive(Default, Reflect, GizmoConfigGroup)]
pub struct NavigationGroup;

impl Plugin for NavigationPlugin {
    fn build(&mut self, app: &mut App) {
        app
            .init_gizmo_group::<NavigationGroup>()
            // ... rest of plugin initialization.
    }
}

以下是将 gizmo 组的配置设置为不同值的方式

// Bevy 0.13.0
set_gizmo_width(mut config_store: ResMut<GizmoConfigStore>) {
    let config = config_store.config_mut::<DefaultGizmoConfigGroup>().0;
    config.line_width = 20.0;

    let navigation_config = config_store.config_mut::<NavigationGroup>().0;
    navigation_config.line_width = 10.0;
}

现在,导航 gizmos 拥有一个完全独立的配置,不会与游戏的 gizmos 发生冲突。

不仅如此,游戏开发者还可以根据自己的意愿将导航 gizmos 与他们自己的调试工具集成并切换。无论是热键、调试覆盖 UI 按钮,还是 RPC 调用。世界是你的牡蛎。

glTF 扩展 #

作者:@CorneliusCornbread

glTF 是一种流行的标准化开放文件格式,用于在不同程序之间存储和共享 3D 模型和场景。不过,标准的问题在于您最终会想要对其进行自定义,即使只是一点点,以更好地满足您的需求。Khronos Group 预见了这一点,明智地定义了一种标准化方式来定制格式,称为 **扩展**。

扩展可以轻松地从其他工具(如 Blender)导出,并包含 各种 其他有用的信息:从尖端的基于物理的材料信息(如各向异性)到性能提示(如如何实例化网格)。

由于 Bevy 将加载的 glTF 解析成我们自己的基于实体的对象层次结构,因此在您想做新的渲染操作时访问这些信息可能很困难!凭借 CorneliusCornbread 的更改,您可以配置加载器以将 glTF 文件的原始副本与您的加载资产一起存储,允许您根据自己的需要解析和重新处理这些信息。

资产转换器 #

作者:@thepackett, @RyanSpaker

资产处理的核心是实现 Process 特性,它接收一些表示资产的字节数据,对其进行转换,然后返回处理后的字节数据。但是,手动实现 Process 特性有点复杂,因此编写了一个通用的 LoadAndSave<L: AssetLoader, S: AssetSaver> Process 实现,以使资产处理更加符合人体工程学。

使用 LoadAndSave Process 实现,以前的资产处理管道有四个阶段

  1. AssetReader 读取一些资产源(文件系统、http 等)并获取资产的字节数据。
  2. AssetLoader 读取字节数据并将其转换为 Bevy Asset
  3. AssetSaver 获取 Bevy Asset,对其进行处理,然后将其转换回字节数据。
  4. 然后,AssetWriter 将资产字节数据写回资产源。

AssetSaver 负责转换资产并将其转换为字节数据。但是,这对于代码可重用性来说有点问题。每次您想要转换某个资产(例如图像)时,都需要重写将资产转换为字节数据的部分。为了解决这个问题,AssetSaver 现在仅负责将资产转换为字节数据,并引入了负责转换资产的 AssetTransformer。添加了一个新的 LoadTransformAndSave<L: AssetLoader, T: AssetTransformer, S: AssetSaver> Process 实现来利用新的 AssetTransformer

使用 LoadTransformAndSave Process 实现的新资产处理管道有五个阶段

  1. AssetReader 读取一些资产源(文件系统、http 等)并获取资产的字节数据。
  2. AssetLoader 读取字节数据并将其转换为 Bevy Asset
  3. AssetTransformer 获取资产并以某种方式对其进行转换。
  4. AssetSaver 获取 Bevy Asset 并将其转换回字节数据。
  5. 然后,AssetWriter 将资产字节数据写回资产源。

除了具有更好的代码可重用性之外,此更改还鼓励为各种常见资产类型编写 AssetSaver,这可以用于将运行时资产保存功能添加到 AssetServer 中。

以前的 LoadAndSave Process 实现仍然存在,因为在某些情况下资产转换步骤是不必要的,例如在将资产保存为压缩格式时。

请参阅 资产处理示例,详细了解如何使用 LoadTransformAndSave 来处理自定义资产。

实体优化 #

作者:@Bluefinger, @notverymoe, @scottmcm, @james7132, @NathanSWard

Entity(Bevy 用于实体的 64 位唯一标识符)在本周期中进行了多次更改,为关系奠定了更多基础,并提供了相关且不错的性能优化。这里的工作涉及大量深入研究编译器代码生成/汇编输出,运行大量基准测试并进行测试,以确保所有更改都不会造成破坏或重大问题。虽然这里的工作主要处理安全代码,但底层假设发生了很多变化,可能会影响其他地方的代码。这是 Bevy 0.13 中最侧重于“微优化”的一组更改。

  • #9797:创建了一个统一的标识符类型,为我们使用相同快速、复杂的代码铺平了道路,无论是在我们的 Entity 类型中,还是在期待已久的关联关系中
  • #9907:允许我们以与 Entity 相同的位数存储 Option<Entity>,方法是更改 Entity 类型的布局,为 None 变体保留一个 u64
  • #10519:将我们切换到为 Entity 手动制作的 PartialEqHash 实现,以提高速度并在热循环中节省指令
  • #10558:将 #9907#10519 的方法结合起来,进一步优化 Entity 的布局,并优化了我们的 PartialOrdOrd 实现!
  • #10648:进一步优化了我们的实体散列,更改了我们在散列中进行乘法的机制,以在优化后的编译器输出中节省一条宝贵的汇编指令

也应向在 #2372#3788 中进行类似工作的作者表示衷心的感谢:虽然他们的工作最终没有被合并,但它对这些最新的更改来说是一个非常宝贵的灵感和先前艺术的来源。

Benchmark results of optimization work

以上结果显示了我们从开始(optimised_eq 是第一个引入基准测试的 PR)到现在的所有优化都已到位(optimised_entity)。所有方面都取得了改进,具有明显的性能优势,这些优势应该会影响代码库的多个区域,而不仅仅是在散列实体时。

链接的 PR 中包含大量清晰易懂的细节,其中还有一些关于汇编输出分析的有趣内容。如果您感兴趣,可以在后台打开一些新标签!

Query::for_each 移植到 QueryIter::fold 覆盖 #

作者:@james7132

目前,为了充分利用对查询的迭代性能,必须使用 Query::for_each 来利用编译器可以应用的自动矢量化和内部迭代优化。但是,这在 Rust 中并不符合惯例,而且不是迭代器方法,因此您无法在迭代器链上使用它。然而,对于某些迭代器方法,有可能获得相同的优势,为此 #6773 by @james7132 试图实现这一点。通过对 QueryIter::fold 提供覆盖,可以移植 Query::for_each 的迭代策略,使 Query::iter 等方法能够获得相同的收益。目前并非所有迭代器方法都受益于此,因为它们需要覆盖 QueryIter::try_fold,但目前这仍然是仅限夜间版本的优化。这种相同的方法在 Rust 标准库中也有体现。

这在几个方面去除了重复的代码,例如不再需要同时使用 Query::for_eachQuery::for_each_mut,因为只需要调用 Query::iterQuery::iter_mut 即可。所以像这样的代码:

fn some_system(mut q_transform: Query<&mut Transform, With<Npc>>) {
    q_transform.for_each_mut(|transform| {
        // Do something...
    });
}

变成

fn some_system(mut q_transform: Query<&mut Transform, With<Npc>>) {
    q_transform.iter_mut().for_each(|transform| {
        // Do something...
    });
}

还比较了主分支与 PR 中的汇编输出,没有发现旧 Query::for_each 与新 QueryIter::for_each() 输出之间有任何明显的差异,验证了这种方法并确保应用了内部迭代优化。

此外,Query::par_for_each 中的相同内部迭代优化现在重用了 for_each 中的代码,也去除了那里的重复代码,并允许用户使用 par_iter().for_each()。总的来说,这意味着不再需要 Query::for_eachQuery::for_each_mutQuery::_par_for_eachQuery::par_for_each_mut,因此这些方法在 0.13 版本中已弃用,将在 0.14 版本中删除。

减少 TableRowas 转换 #

作者:@bushrat011899

我们 ECS 内部并非所有改进都集中在性能上。进行了一些小的更改,以提高类型安全性并清理部分代码库,减少各种调用站点对 TableRow 进行的 as 转换。as 转换的问题在于,在某些情况下,转换会通过静默截断值而失败,这可能会导致通过访问错误的行等情况造成混乱。#10811 by @bushrat011899 提出清理 TableRow 周围的 API,提供由 assert 支持的便捷方法,以确保转换操作永远不会失败,或者如果失败,它们会正确地出现恐慌。

自然,在潜在的热点代码路径中添加断言会引起一些担忧,需要进行大量的基准测试工作,以确认是否存在回归以及回归程度。通过仔细放置新的 assert,检测到的回归情况约为 0.1%,远在噪声范围之内。但带来的好处是一个更不容易出错的 API 和更健壮的代码。对于像 bevy_ecs 这样的复杂不安全代码库,任何微小的改进都有帮助。

事件生命周期更长 #

事件是将数据传递到系统和系统之间的一个有用工具。

在内部,Bevy 事件是双缓冲的,因此一旦缓冲区交换两次,给定的事件将被静默丢弃。Events<T> 资源就是这样设置的,以便事件在可预测的时间段后被丢弃,防止它们的队列无限增长并导致内存泄漏。

在 0.12.1 之前,事件队列在每次更新(即每帧)时交换。这对于在 FixedUpdate 中有逻辑的游戏来说是一个问题,因为这意味着事件通常会在下一个 FixedUpdate 中的系统读取它们之前消失。

Bevy 0.12.1 将交换节奏改为“每次运行 FixedUpdate 一次或多次的更新”(仅当安装了 TimePlugin 时)。此更改确实解决了最初的问题,但随后在另一个方向上造成了问题。用户惊讶地发现,他们的一些带有 run_if 条件的系统会迭代比预期更旧的事件。(事后看来,我们应该将其视为重大变更,并将其推迟到此版本)。此更改还引入了一个错误(在本版本中已修复),其中只有一种类型的事件被丢弃。

一个提出的未来解决方案是,使用事件时间戳来更改 EventReader<T> 可见的事件默认范围,从而解决 UpdateFixedUpdate 之间持续存在的但无意中的耦合。这样,Update 中的系统将跳过比一帧更旧的任何事件,而 FixedUpdate 中的系统仍然可以看到它们。

目前,可以通过简单地删除 EventUpdateSignal 资源来恢复 <=0.12.0 的行为。

fn main() {
    let mut app = App::new()
        .add_plugins(DefaultPlugins);
    
    /* ... */

    // If this resource is absent, events are dropped the same way as <=0.12.0.
    app.world.remove_resource::<EventUpdateSignal>();
    
    /* ... */

    app.run();
}

接下来是什么? #

我们还有很多工作正在进行中!其中一部分可能会在Bevy 0.14 中发布。

查看 Bevy 0.14 里程碑,了解贡献者正在为Bevy 0.14 集中精力完成的当前工作的最新列表。

更多编辑器实验 #

在才华横溢的 JMS55 的带领下,我们开放了一个自由形式的 游乐场,来定义和回答关于 bevy_editor 设计的 关键问题:不是通过讨论,而是通过具体的原型设计。我们应该使用一个进程内编辑器(对游戏崩溃的鲁棒性较差)还是一个外部编辑器(更复杂)?我们应该发布一个编辑器二进制文件(非常适合非程序员)还是将其嵌入游戏本身(可高度定制)?让我们通过实践来找出答案!

有一些令人难以置信的模型、功能原型和与第三方编辑器相关的项目。一些亮点

一个图形化的 Bevy 编辑器 UI 模型 (1) bevy_editor_mockup
一个使用 `bevy_egui` 构建的基于节点的动画图编辑器 (2) bevy_animation_graph
来自 `space_editor` 的屏幕截图,展示了一个带有 Gizmo 的功能性场景编辑器 (3) space_editor
来自 Blender 的屏幕截图,其中 Blender UI 修改了 Bevy 组件值 (4) bevy_components
一个基于 Web 的编辑器的录制视频,实时更改 Bevy 实体 (5) bevy_remote
  1. 由 Discord 上的 @!!&Amy 创建的 Bevy 品牌的编辑器 UI 模型,想象一个基于 ECS 的编辑器 可能是什么样子
  2. bevy_animation_graph:一个完全功能的、由资产驱动的动画图 crate,它有自己的基于节点的编辑器,适用于 Bevy
  3. space_editor:一个经过打磨的 Bevy 原生第三方场景编辑器,您可以立即使用它!
  4. Blender_bevy_components_workflow:一套功能强大的工具生态系统,可以让你立即使用 Blender 作为游戏无缝的关卡和场景编辑器。
  5. @coreh基于反射的远程协议 的实验,结合一个交互式基于 Web 的编辑器,允许开发人员从其他进程、语言甚至设备检查和控制他们的 Bevy 游戏!立即试用

看到这些进展真的令人激动,我们渴望将这些能量和经验投入到官方的一方努力中。

bevy_dev_tools #

顺利进行游戏开发的秘诀是强大的工具。是时候为 Bevy 开发者提供他们需要的工具来检查、调试和分析他们的游戏,作为第一方体验的一部分了。从 FPS 计到系统步进,再到与出色的 bevy-inspector-egui 相当的第一方等效物:将这些工具放在 Bevy 本身中,有助于我们对其进行打磨,为新用户指明正确的方向,并让我们在 bevy_editor 本身中使用它们。

新的场景格式 #

场景 是 Bevy 用于将 ECS 数据序列化到磁盘的通用答案:跟踪实体、组件和资源,用于保存游戏和加载预制关卡。但是,现有的基于 .ron 的场景格式很难手动编写,过于冗长且脆弱;对你的代码(或你的依赖项的代码!)的更改会迅速使保存的场景失效。Cart 一直在酝酿一个 修订后的场景格式,该格式具有紧密的 IDE 和代码集成,解决了这些问题,并使在 Bevy 中创作内容(包括 UI!)成为一种乐趣。无论你是编写代码、编写场景文件还是从 GUI 生成场景文件。

bevy_ui 改进 #

bevy_ui 有不少问题和局限性,既有普通的,也有架构上的;但是,我们能够并且正在做一些切实的事情来改进它:改进的场景格式为定义布局时结束样板提供了帮助,圆角 边框 只需要审阅者稍加关注,而且强大的且广受欢迎的 [bevy_mod_picking] 中的对象拾取计划将被上游用于 UI 和游戏玩法。今天已经存在大量令人惊叹的 第三方 UI 解决方案,从这些解决方案中学习,并致力于 UI 逻辑和响应能力的核心架构是重中之重。

Meshlet 渲染 #

将网格分割成称为 meshlet 的三角形簇,这会带来许多效率提升。在 0.13 开发周期中,我们在这项功能上取得了 很大进展。我们实现了一个 GPU 驱动的 meshlet 渲染器,它可以扩展到更多的三角形密集场景,而 CPU 负载要低得多。但是,内存使用量非常高,我们还没有实现 LOD 或压缩。与其发布半成品,我们将会继续迭代,并非常高兴地(希望)在未来发布中为您提供此功能。

The Stanford dragon mesh rendered as meshlet clusters

朝着关系迈进的稳步步伐 #

实体-实体关系,即直接在 ECS 中跟踪和管理实体之间连接的能力,多年来一直是人们要求最多的 ECS 功能之一。遵循 flecs 开辟的道路#ecs-dev 中的疯狂科学家们正在稳步地 重塑我们的内部结构尝试外部实现,并发布构建快速、健壮和符合人体工程学解决方案所需的通用构建块(如动态查询或 生命周期钩子)。

支持 Bevy #

赞助有助于使我们在 Bevy 上的工作可持续。如果你相信 Bevy 的使命,请考虑 赞助我们……任何帮助都非常感谢!

捐赠 心形图标

贡献者 #

Bevy 是由 一群人 共同打造的。非常感谢 198 位贡献者,使这次发布(以及相关的文档)成为可能!按随机顺序排列

  • @ickk
  • @orph3usLyre
  • @tygyh
  • @nicopap
  • @NiseVoid
  • @pcwalton
  • @homersimpsons
  • @Henriquelay
  • @Vrixyz
  • @GuillaumeGomez
  • @porkbrain
  • @Leinnan
  • @IceSentry
  • @superdump
  • @solis-lumine-vorago
  • @garychia
  • @tbillington
  • @Nilirad
  • @JMS55
  • @kirusfg
  • @KirmesBude
  • @maueroats
  • @mamekoro
  • @NiklasEi
  • @SIGSTACKFAULT
  • @Olle-Lukowski
  • @bushrat011899
  • @cbournhonesque-sc
  • @daxpedda
  • @Testare
  • @johnbchron
  • @BlackPhlox
  • @MrGVSV
  • @Kanabenki
  • @SpecificProtagonist
  • @rosefromthedead
  • @thepackett
  • @wgxer
  • @mintlu8
  • @AngelOnFira
  • @ArthurBrussee
  • @viridia
  • @GabeeeM
  • @Elabajaba
  • @brianreavis
  • @dmlary
  • @akimakinai
  • @VitalyAnkh
  • @komadori
  • @extrawurst
  • @NoahShomette
  • @valentinegb
  • @coreh
  • @kristoff3r
  • @wackbyte
  • @BD103
  • @stepancheg
  • @bogdiw
  • @doup
  • @janhohenheim
  • @ekropotin
  • @thmsgntz
  • @alice-i-cecile
  • @tychedelia
  • @soqb
  • @taizu-jin
  • @kidrigger
  • @fuchsnj
  • @TimJentzsch
  • @MinerSebas
  • @RomainMazB
  • @cBournhonesque
  • @tripokey
  • @cart
  • @pablo-lua
  • @cuppar
  • @TheTacBanana
  • @AxiomaticSemantics
  • @rparrett
  • @richardhozak
  • @afonsolage
  • @conways-glider
  • @ItsDoot
  • @MarkusTheOrt
  • @DavJCosby
  • @thebluefish
  • @DGriffin91
  • @Shatur
  • @MiniaczQ
  • @killercup
  • @Ixentus
  • @hecksmosis
  • @nvdaz
  • @james-j-obrien
  • @seabassjh
  • @lee-orr
  • @Waridley
  • @wainwrightmark
  • @robtfm
  • @asuratos
  • @Ato2207
  • @DasLixou
  • @SludgePhD
  • @torsteingrindvik
  • @jakobhellermann
  • @fantasyRqg
  • @johanhelsing
  • @re0312
  • @ickshonpe
  • @BorisBoutillier
  • @lkolbly
  • @Friz64
  • @rodolphito
  • @TheBlckbird
  • @HeyZoos
  • @nxsaken
  • @UkoeHB
  • @GitGhillie
  • @ibotha
  • @ManevilleF
  • @andristarr
  • @josfeenstra
  • @maniwani
  • @Trashtalk217
  • @benfrankel
  • @notverymoe
  • @simbleau
  • @aevyrie
  • @Dig-Doug
  • @IQuick143
  • @shanecelis
  • @mnmaita
  • @Braymatter
  • @LeshaInc
  • @esensar
  • @Adamkob12
  • @Kees-van-Beilen
  • @davidasberg
  • @andriyDev
  • @hankjordan
  • @Jondolf
  • @SET001
  • @hxYuki
  • @matiqo15
  • @capt-glorypants
  • @hymm
  • @HugoPeters1024
  • @RyanSpaker
  • @bardt
  • @tguichaoua
  • @SkiFire13
  • @st0rmbtw
  • @Davier
  • @mockersf
  • @antoniacobaeus
  • @ameknite
  • @Pixelstormer
  • @bonsairobo
  • @matthew-gries
  • @NthTensor
  • @tjamaan
  • @Architector4
  • @JoJoJet
  • @TrialDragon
  • @Gadzev
  • @eltociear
  • @scottmcm
  • @james7132
  • @CorneliusCornbread
  • @Aztro-dev
  • @doonv
  • @Malax
  • @atornity
  • @Bluefinger
  • @kayhhh
  • @irate-devil
  • @AlexOkafor
  • @kettle11
  • @davidepaci
  • @NathanSWard
  • @nfagerlund
  • @anarelion
  • @laundmo
  • @nelsontkq
  • @jeliag
  • @13ros27
  • @Nathan-Fenner
  • @softmoth
  • @xNapha
  • @asafigan
  • @nothendev
  • @SuperSamus
  • @devnev
  • @RobWalt
  • @ThePuzzledDev
  • @rafalh
  • @dubrowgn
  • @Aceeri

完整变更日志 #

上面提到的更改仅是我们本轮更新中最引人注目、影响最大的更改。还有无数的错误修复、文档更改和 API 易用性调整也包含在内。有关更改的完整列表,请查看下面列出的 PR。

A-Rendering + A-Windowing #

A-Animation + A-Reflection #

A-Assets #

A-Core + A-App #

A-Accessibility #

A-Transform #

A-ECS + A-Hierarchy #

A-Text #

A-Assets + A-UI #

A-Utils + A-Time #

A-Rendering + A-Assets #

A-Physics #

A-ECS + A-Editor + A-App + A-Diagnostics #

A-Reflection + A-Scenes #

A-Audio + A-Windowing #

A-Build-System + A-Meta #

A-ECS + A-Time #

A-Meta #

A-Time #

A-Assets + A-Reflection #

A-Diagnostics + A-Utils #

A-Windowing + A-App #

A-ECS + A-Scenes #

A-Hierarchy #

A-ECS + A-App #

A-Transform + A-Math #

A-UI + A-Text #

A-Input #

A-Rendering + A-Diagnostics #

A-Rendering #

A-场景 #

A-实用程序 #

A-UI #

A-资产 + A-诊断 #

A-音频 + A-反射 #

A-音频 #

A-任务 #

A-ECS #

A-Rendering + A-Animation #

A-ECS + A-Meta #

A-Rendering + A-UI #

A-Math #

A-Build-System #

A-Gizmos #

A-Rendering + A-Math #

A-Core #

A-Windowing #

A-UI + A-Transform + A-Text #

A-Animation #

A-ECS + A-Pointers #

A-ECS + A-Utils #

A-Reflection #

A-ECS + A-Tasks #

A-Pointers #

A-ECS + A-Reflection #

A-Reflection + A-Gizmos #

没有区域标签 #

A-App #

A-Diagnostics #

A-Rendering + A-ECS #

A-Assets + A-Math #