新闻

Bevy 0.14

发布日期:2024 年 7 月 4 日,作者:Bevy 贡献者

感谢 **256** 位贡献者、**993** 个拉取请求、社区审阅者以及我们慷慨的捐赠者,我们很高兴地宣布 **Bevy 0.14** 在 crates.io 上发布!

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

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

自从我们几个月前发布以来,我们添加了许多新功能、错误修复和质量改进,但以下是一些亮点

  • 虚拟几何体:将网格预处理成“网格片”,使渲染大量几何体变得高效
  • 锐利的屏幕空间反射:近似实时光线追踪屏幕空间反射
  • 景深:使特定深度的物体“失焦”,模仿物理镜头的行为
  • 逐物体运动模糊:对相对于摄像机快速移动的物体进行模糊处理
  • 体积雾/光照:模拟 3D 空间中的雾,使灯光能够产生美丽的光线轴
  • 胶片色彩分级:使用一组完整的胶片色彩分级工具,微调游戏中的色调映射
  • PBR 各向异性:改善表面渲染,这些表面的粗糙度沿网格的切线和副切线方向变化,例如刷过的金属和头发
  • 自动曝光:配置摄像机以根据它们所观察到的内容动态调整曝光
  • 点光源的 PCF:使点光源阴影变得平滑,改善其质量
  • 动画混合:我们的新型低级动画图增加了对动画混合的支持,并为第一方和第三方图形、资产驱动的动画工具奠定了基础。
  • ECS 钩子和观察者:自动(且立即)响应任意事件,例如组件添加和删除
  • 更好的色彩:类型安全的色彩使操作的色彩空间变得清晰,并提供了一系列有用的方法。
  • 计算状态和子状态:使用这些对 States 抽象的类型安全扩展,对复杂应用程序状态进行建模变得轻而易举。
  • 圆角:bevy_ui 的最粗糙的边缘中,您现在可以程序化地设置 UI 元素的角半径。

Bevy 0.14 是第一次使用 **发布候选版** 流程进行准备的,以确保您能够放心地立即升级。我们与插件作者和普通用户紧密合作,捕捉关键错误,完善新功能的不足之处,并改进迁移指南。在准备修复程序时,我们已 在 crates.io 上发布了新的发布候选版,让核心生态系统箱子进行更新,并密切关注关键问题。非常感谢 所有提供帮助的人:这些努力是将 Bevy 打造成团队无论大小都可以信赖的可靠工具的关键步骤。

虚拟几何体(实验性) #

  • 作者:@JMS55、@atlv24、@zeux、@ricky26
  • PR #10164

经过几个月的努力,我们很高兴地为您带来新的虚拟几何体功能的实验性版本!

这种新的渲染功能与虚幻引擎 5 的 Nanite 渲染器非常相似。您可以获取一个非常高精度的网格,在构建时对其进行预处理以生成 MeshletMesh,然后在运行时渲染大量的几何体——远超 Bevy 的标准渲染器所能支持的范围。不需要显式的 LOD——这一切都是自动的,并且几乎无缝。

此功能仍在开发中,与 Bevy 的标准渲染器相比,它有一些限制,因此请务必阅读文档并报告遇到的任何错误。我们还有很多工作要做,所以请期待未来版本中更多的性能改进(以及相关的重大变更)!

请注意,此功能不使用 GPU“网格着色器”,因此旧的 GPU 目前可以兼容。但是,不建议使用旧的 GPU,并且它们可能会在不久的将来被取消支持。

除了以下用户指南之外,请查看

想要使用虚拟几何体的用户应该在运行时使用 meshlet Cargo 功能进行编译,在构建时使用 meshlet_processor Cargo 功能对网格进行预处理,使其转换为网格片专用格式 (MeshletMesh),这是网格片渲染器使用的格式。

启用网格片功能将解锁一个新模块:bevy::pbr::experimental::meshlet.

第一步,将 MeshletPlugin 添加到您的应用程序

app.add_plugins(MeshletPlugin);

接下来,将您的 Mesh 预处理成 MeshletMesh。目前,这需要通过 MeshletMesh::from_mesh() 手动完成(再次强调,您需要启用 meshlet_processor 功能)。此步骤相当缓慢,应该在事先完成一次,然后保存到资产文件中。请注意,对支持的网格和材质类型存在限制,请务必阅读文档。

计划通过 Bevy 的资产预处理系统实现自动 GLTF/场景转换,但不幸的是,它没有及时完成这次发布。目前,您需要自己创建资产转换和管理系统。如果您找到了一个好的系统,请告诉我们!

现在,生成您的实体。与 MaterialMeshBundle 相似,有一个 MaterialMeshletMeshBundle,它使用 MeshletMesh 而不是传统的 Mesh

commands.spawn(MaterialMeshletMeshBundle {
    meshlet_mesh: meshlet_mesh_handle.clone(),
    material: material_handle.clone(),
    transform,
    ..default()
});

最后,关于材质说明一下。Meshlet 实体使用与普通网格实体相同的 Material 特性,但标准的材质方法未使用。取而代之的是 3 种新方法:meshlet_mesh_fragment_shadermeshlet_mesh_prepass_fragment_shadermeshlet_mesh_deferred_fragment_shader。支持所有 3 种方法,包括前向渲染、带预渲染的前向渲染和延迟渲染。

但是请注意,无法访问顶点着色器。Meshlet 渲染使用一个硬编码的顶点着色器,无法更改。

Meshlet 材质的实际片段着色器代码与普通网格实体的片段着色器代码基本相同。主要区别在于,您应该使用以下代码,而不是:

@fragment
fn fragment(vertex_output: VertexOutput) -> @location(0) vec4<f32> {
    // ...
}

#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output

@fragment
fn fragment(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
    let vertex_output = resolve_vertex_output(frag_coord);
    // ...
}

锐利屏幕空间反射 #

将此图片拖动以进行比较

No SSRSSR

屏幕空间反射 (SSR) 通过在深度缓冲区中进行光线步进并从最终渲染帧中复制样本,来近似实时反射。我们的初始实现相对简单,目的是提供一个灵活的基础,以便于在此基础上构建,但它基于 Tomasz Stachowiak 创建的生产级 光线步进代码,他是独立游戏大作 Tiny Glade 的创作者之一。因此,需要注意一些事项

  1. 目前,此功能构建在延迟渲染器之上,并且目前仅在该模式下受支持。向前屏幕空间反射是可能的,尽管并不常见(例如,Doom Eternal 使用了它们);但是,它们需要从上一帧进行追踪,这会增加复杂性。此补丁为在向前渲染路径中实现 SSR 留下了空间,但本身并没有这样的实现。
  2. 屏幕空间反射在 WebGL 2 中不受支持,因为它们需要从深度缓冲区中采样,而 naga 由于存在错误无法执行此操作(sampler2DShadow 错误地生成了 sampler2D;这就是景深在该平台上禁用的原因)。
  3. 根本没有执行时间滤波或模糊。因此,SSR 目前仅在非常低粗糙度/光滑表面上运行。
  4. 我们不通过分层 Z 缓冲区进行加速,并且反射在全分辨率下进行追踪。因此,您可能会注意到性能问题,具体取决于您的场景和硬件。

要将屏幕空间反射添加到相机中,请插入 ScreenSpaceReflectionsSettings 组件。除了 ScreenSpaceReflectionsSettings 之外,还必须存在 DepthPrepassDeferredPrepass,才能显示反射。方便的是,ScreenSpaceReflectionsBundle 将所有这些捆绑在一起!虽然 ScreenSpaceReflectionsSettings 带有合理的默认值,但它还包含艺术家可以调整的多个设置。

体积雾和体积光照(光束/上帝之光) #

并非所有雾都是平等的。Bevy 的现有实现涵盖了 距离雾,它速度快、简单,但并不特别逼真。

在 Bevy 0.14 中,这是基于 体积光照 的体积雾的补充,它使用实际的 3D 空间来模拟雾,而不是简单地使用与相机的距离。正如您可能预期的那样,这既更漂亮也更耗费计算资源!

特别是,这允许创建惊人美丽的“上帝之光”(更准确地说是黄昏光线),从雾中照射出来。

将此图片拖动以进行比较

Without Volumetric FogWith Volumetric Fog

Bevy 的算法(作为后期处理效果实现)结合了 ScratchapixelAlexandre Pestana 的博客文章 中描述的技术。它使用屏幕空间中的光线步进(由 h3r2tic 移植到 WGSL),转换为阴影贴图空间以进行采样,并与基于物理的吸收和散射建模相结合。Bevy 使用广泛使用的 Henyey-Greenstein 相位函数来模拟不对称性;这本质上允许光束在用户查看它们时淡入淡出。

要将体积雾添加到场景中,请将 VolumetricFogSettings 添加到相机中,并将 VolumetricLight 添加到您希望成为体积光的方向光中。 VolumetricFogSettings 具有许多设置,允许您定义模拟的精度以及雾的外观。目前,仅支持与具有阴影贴图的方向光交互。请注意,效果的开销与使用的方向光数量直接相关,因此谨慎使用 VolumetricLight 以获得最佳结果。

使用我们的 volumetric_fog 示例 亲身体验一下。

每个物体运动模糊 #

  • 作者:@aevyrie、@torsteingrindvik
  • PR #9924

我们添加了一种后期处理效果,可以在运动方向上模糊快速移动的物体。我们的实现使用运动向量,这意味着它适用于 Bevy 的内置 PBR 材质、蒙皮网格或任何其他写入运动向量和深度的对象。该效果用于传达高速运动,否则在图像完美锐利时看起来像是闪烁或瞬移。

模糊随物体相对于相机的运动而缩放。如果相机跟踪快速移动的物体(例如,车辆),则车辆将保持清晰,而静止物体将被模糊。相反,如果相机指向一个静止物体,而一个快速移动的车辆穿过画面,那么只有快速移动的物体会被模糊。

该实现使用 相机快门角 进行配置,它对应于虚拟快门在帧期间打开的时间。实际上,这意味着该效果随帧率而缩放,因此以高刷新率运行的用户不会受到过度模糊的影响。

您可以通过将 MotionBlurBundle 添加到相机实体来启用运动模糊,如我们的 motion blur 示例 中所示。

电影色彩分级 #

艺术家希望为他们的游戏获得完全正确的外观,而颜色起着至关重要的作用。

为了支持这一点,Bevy 的 现有色调映射工具 已扩展到包含一组完整的电影色彩分级工具。除了 基本色调映射 之外,您现在还可以配置

  • 白点调整。这受 Unity 实现此功能的启发,但进行了简化和优化。温度和色调控制 CIE 1931 的 x 和 y 色度值的调整。与 Unity 一致,这些调整是在 LMS 颜色空间中相对于 D65 标准光源进行的。
  • 色调旋转:将 RGB 值转换为 HSV,改变色调,然后转换回来。
  • 颜色校正:允许根据标准 ASC CDL 组合函数调整 gamma、增益和提升值。这可以分别针对阴影、中间调和高光进行。为了避免颜色变化突然,在图像的不同部分之间使用了一个小的交叉淡入淡出。

我们尽可能地遵循了 Blender 的实现,以确保您在建模软件中看到的内容与您在游戏中看到的内容相匹配。

A very orange image of a test scene, with controls for exposure, temperature, tint and hue. Saturation, contrast, gamma, gain, and lift can all be configured for the highlights, midtones, and shadows separately.

我们提供了一个新的 color_grading 示例,它带有一个闪亮的 GUI,用于更改所有色彩分级设置。非常适合复制粘贴到您自己的游戏的开发工具中,并使用这些设置进行玩耍!请注意,所有这些设置都可以在运行时更改:让艺术家可以控制场景的确切气氛,或者根据天气或一天中的时间动态地改变气氛。

自动曝光 #

Bevy 0.13 以来,您可以 配置相机的 EV100,这使您能够以基于物理的方式调整相机的曝光。这也使您能够动态更改各种效果的曝光值。但是,这是一个手动过程,需要您自己调整曝光值。

Bevy 0.14 引入了 自动曝光,它会根据场景的亮度自动调整相机的曝光。当您想要营造非常高动态范围的感觉时,这很有用,因为您的眼睛也会适应亮度的大幅变化。请注意,这不是手动调整曝光值的替代品,而是一种额外的工具,您可以使用它在亮度快速变化时营造戏剧性的效果。查看从 示例 中录制的视频,以查看它的实际效果!

Bevy 的自动曝光是通过在后期处理步骤中对场景亮度进行 直方图 来实现的。然后根据直方图的平均值调整曝光。由于直方图是使用计算着色器计算的,因此 自动曝光在 WebGL 上不可用。它默认情况下也不启用,因此您需要将 AutoExposurePlugin 添加到您的应用程序中。

自动曝光由 AutoExposureSettings 组件控制,您可以将其添加到相机实体中。您可以配置一些内容

  • 曝光可以改变的 F 档 范围
  • 曝光改变的 速度
  • 可选的 测光蒙版,它使您可以例如,对图像的中心赋予更大的权重。
  • 可选的直方图 滤波器,它使您可以忽略非常亮或非常暗的像素。

快速景深 #

  • 作者:@pcwalton、@alice-i-cecile、@Kurble
  • PR #13009

在渲染中,景深 是一种模拟 物理透镜的局限性 的效果。由于光的运作方式,透镜(如人眼或胶片相机的透镜)只能聚焦在距其特定范围(深度)内的物体上,导致所有其他物体变得模糊并失焦。

Bevy 现在附带此效果,它作为后期处理着色器实现。有两种选项可用:快速的 Gaussian 模糊或更逼真的六边形散景技术。散景模糊通常比 Gaussian 模糊更美观,因为它更准确地模拟了相机的效果。散景圆的形状由光圈叶片的数量决定。在本例中,我们使用六边形,这通常被认为是较低质量相机的特点。

将此图片拖动以进行比较

No Depth of FieldBokeh Depth of Field

模糊量通常由 f 档 指定,我们用它来从胶片尺寸和 视野 计算 焦距。默认情况下,我们使用 f/1 f 档和对应于经典 Super 35 胶片格式的胶片尺寸来模拟标准电影相机。开发人员可以根据需要自定义这些值。

要查看此新 API,请查看专门的 depth_of_field 示例

PBR 各向异性 #

各向异性材料 随运动轴变化,例如木材沿纹理方向和逆纹理方向的性能差异很大。但在基于物理的渲染中,**各向异性** 特指一种允许粗糙度沿网格的切线和副切线方向变化的特性。实际上,这会导致镜面光线拉伸成线条而不是圆形光瓣。这对于模拟磨砂金属、头发和类似表面非常有用。各向异性支持是主要游戏和图形引擎的常见功能;Unity、Unreal、Godot、three.js 和 Blender 都在不同程度上支持它。

将此图片拖动以进行比较

Without AnisotropyWith Anisotropy

已向 StandardMaterial 添加了两个新参数:anisotropy_strengthanisotropy_rotation。各向异性强度取值范围为 0 到 1,表示网格切线和副切线之间的粗糙度差异程度。实际上,它控制镜面高光拉伸的程度。各向异性旋转允许粗糙度方向与模型的切线方向不同。

除了这两个固定参数外,还可以提供各向异性纹理,使用 KHR_materials_anisotropy 指定的线性纹理格式。

与往常一样,请在相应的 anisotropy 示例 中试用一下。

点光源的百分比更近滤波 (PCF) #

百分比更近滤波是一种标准的抗锯齿技术,用于获得更柔和、更不生硬的阴影。为此,我们使用高斯核从阴影贴图中对感兴趣像素附近的像素进行采样,对结果进行平均,从而减少阴影中突然的过渡。

因此,Bevy 的点光源现在看起来更柔和、更自然,而无需更改最终用户代码。与之前一样,您可以通过设置 3D 相机上的 ShadowFilteringMethod 组件来配置用于对阴影进行抗锯齿的具体策略。

将此图片拖动以进行比较

Without PCF filteringWith PCF filtering

百分比更近阴影的全面支持 正在进行中:测试和评论,与往常一样,非常欢迎。

亚像素形态抗锯齿 (SMAA) #

  • 作者:@pcwalton、@alice-i-cecile
  • PR #13423

锯齿边缘是游戏开发人员的梦魇:为了解决这些问题,而不降低图像质量,已经发明了各种各样的抗锯齿技术,并且仍在使用。除了 MSAAFXAATAA 外,Bevy 现在还实现了 SMAA:亚像素形态抗锯齿。

SMAA 是一种 2011 年的抗锯齿技术,它检测图像中的边界,然后对附近的边界像素进行平均,从而消除令人讨厌的锯齿。尽管它已经很老了,但在过去的十年中,它一直是游戏的持续支柱。提供四种质量预设:低、中、高和超高。由于消费级硬件的进步,Bevy 的默认值为高。

您可以从下面的两张图片中看到它与无抗锯齿的效果对比。

将此图片拖动以进行比较

No AASMAA

要了解各种抗锯齿方法的权衡,最好的方法是使用 anti_aliasing 示例 对测试场景进行实验,或者直接在自己的游戏中进行尝试。

可见范围(分层细节级别 / HLOD) #

当我们观察远处物体时,很难看清细节!这一显而易见的事实,在渲染中和现实生活中都是一样的。因此,对远处的物体使用复杂的、高保真度的模型是一种浪费:我们可以用简化的等效模型替换它们的网格。

通过以这种方式自动改变我们模型的**细节级别**(LOD),我们可以渲染更大的场景(或使用更高的绘制距离渲染相同的开放世界),根据模型与玩家的距离动态切换网格。Bevy 现在支持其中最基础的工具之一:**可见范围**(有时称为分层细节级别,因为它允许用户将多个网格替换为单个物体)。

通过在网格实体上设置 VisibilityRange 组件,开发人员可以自动控制其网格在相机中的哪个范围出现和消失,并使用抖动在两个选项之间自动淡入淡出。隐藏网格是在渲染管道的早期进行的,因此此功能可以有效地用于细节级别优化。此外,此功能会针对每个视图进行正确评估,因此不同的视图可以显示不同的细节级别。

请注意,此功能不同于真正的网格 LOD(其中几何体本身会自动简化),后者将在稍后实现。虽然网格 LOD 有助于优化,并且不需要任何额外设置,但它们不如可见范围灵活。游戏通常希望使用网格以外的物体来替换远处的模型,例如八面体或 广告牌 蒙版:首先实现可见范围,可以让用户灵活地从现在开始实施这些解决方案。

您可以在 visibility_range 示例 中查看此功能的用法。

ECS 钩子和观察者 #

我们很高兴在 Bevy 中遍历同质数据块,但并非所有任务都完美适合简单的 ECS 模型。响应更改和/或处理事件是任何应用程序中必不可少的任务,游戏也不例外。

Bevy 已经具有一系列用于处理此问题的工具

  • 缓冲的 Event:多生产者、多消费者队列。灵活且高效,但需要定期轮询作为计划的一部分。事件会在两帧后被丢弃。
  • 通过 AddedChanged 进行更改检测:允许编写可以响应添加或更改的组件的查询。这些查询会线性扫描匹配查询的组件的更改状态,以查看它们是否已添加或更改。
  • RemovedComponents:一种特殊的事件形式,当从实体中删除组件或具有该组件的实体被销毁时触发。

所有这些(以及系统本身!)都使用 "拉式机制":无论是否有监听者,都会发送事件,监听者必须定期轮询以询问是否有任何更改。这是一个有用的模式,我们打算保留它!通过轮询,我们可以批量处理事件,获得更多上下文并提高数据局部性(这会让 CPU 嗡嗡作响)。

但这有一些限制

  • 在触发事件和处理响应之间存在不可避免的延迟
  • 轮询会在每一帧引入少量(但非零)开销

这种延迟是一个关键问题

  • 数据(例如索引或层次结构)即使在短暂的时间内也可能存在于无效状态
  • 我们无法在一个周期内处理任意事件链或递归逻辑

为了克服这些限制,**Bevy 0.14** 引入了**组件生命周期钩子**和**观察者**:两种互补的“推式”机制,灵感来自始终出色的 flecs ECS。

组件生命周期钩子

组件钩子 是为特定组件类型(作为 Component 特性实现的一部分)注册的函数(能够与 ECS 世界进行交互),这些函数会在响应“组件生命周期事件”时自动运行,例如当该组件被添加、覆盖或删除时。

对于给定的组件类型,只能为给定的生命周期事件注册一个钩子,并且不能覆盖它。

钩子用于强制与该组件相关的约束(例如:维护索引或层次结构的正确性)。钩子不能被删除,并且始终优先于观察者:它们在任何添加或插入观察者之前运行,但在任何删除观察者之后运行。因此,可以将它们视为更接近于构造函数和析构函数,更适合于维护关键的安全或正确性约束。钩子也比观察者快一些,因为它们的灵活性较低,意味着涉及的查找次数较少。

让我们检查一个简单的示例,在这个示例中,我们关心维护约束:一个实体(带有 Target 组件)目标另一个实体(带有 Targetable 组件)。

#[derive(Component)]
struct Target(Option<Entity>);

#[derive(Component)]
struct Targetable {
    targeted_by: Vec<Entity>
};

我们希望在目标实体被销毁时自动清除 Target:我们该怎么做呢?

如果我们使用基于拉式的方案(在本例中为 RemovedComponents),则在实体被销毁和 Target 组件被更新之间可能存在延迟。我们可以使用钩子消除这种延迟!

让我们看看在 Targetable 上使用钩子时的样子

// Rather than a derive, let's configure the hooks with a custom
// implementation of Component
impl Component for Targetable {
    const STORAGE_TYPE: StorageType = StorageType::Table;

    fn register_component_hooks(hooks: &mut ComponentHooks) {
        // Whenever this component is removed, or an entity with
        // this component is despawned...
        hooks.on_remove(|mut world, targeted_entity, _component_id|{
            // Grab the data that's about to be removed
            let targetable = world.get::<Targetable>(targeted_entity).unwrap();
            for targeting_entity in targetable.targeted_by {
                // Track down the entity that's targeting us
                let mut targeting = world.get_mut::<Target>(targeting_entity).unwrap();
                // And clear its target, cleaning up any dangling references
                targeting.0 = None;
            }
        })
    }
}

观察者

观察者是按需运行的系统,用于监听“触发”的事件。这些事件可以针对特定实体触发,也可以“全局”触发(没有实体目标)。

与钩子相比,观察者是一种灵活的工具,用于更高级别的应用程序逻辑。它们可以观察何时触发用户定义的事件。

#[derive(Event)]
struct Message {
    text: String
}

world.observe(|trigger: Trigger<Message>| {
    println!("{}", trigger.event().message.text);
});

观察者会在触发它们正在监听的事件时立即运行

// All registered `Message` observers are immediately run here
world.trigger(Message { text: "Hello".to_string() });

如果事件是通过 Command 触发的,则观察者会在 Command 被刷新时运行

fn send_message(mut commands: Commands) {
    // This will trigger all `Message` observers when this system's commands are flushed
    commands.trigger(Message { text: "Hello".to_string() } );
}

事件也可以使用实体目标触发

#[derive(Event)]
struct Resize { size: usize }

commands.trigger_targets(Resize { size: 10 }, some_entity);

您可以同时为多个实体触发事件

commands.trigger_targets(Resize { size: 10 }, [e1, e2]);

任何目标被触发时,将执行“全局”观察者

fn main() {
    App::new()
        .observe(on_resize)
        .run()
}

fn on_resize(trigger: Trigger<Resize>, query: Query<&mut Size>) {
    let size = query.get_mut(trigger.entity()).unwrap();
    size.value = trigger.event().size;
} 

请注意,观察者可以使用系统参数,例如 Query,就像普通系统一样。

您还可以添加仅对特定实体运行的观察者

commands
    .spawn(Widget)
    .observe(|trigger: Trigger<Resize>| {
        println!("This specific widget entity was resized!");
    });

观察者实际上只是一个带有 Observer 组件的实体。上面使用的所有 observe() 方法只是生成新观察者实体的简写。这就是“全局”观察者实体的样子

commands.spawn(Observer::new(|trigger: Trigger<Message>| {}));

同样,监听特定实体的观察者看起来像这样

commands.spawn(
    Observer::new(|trigger: Trigger<Resize>| {})
        .with_entity(some_entity)
);

此 API 使管理和清理观察者变得很容易。它还支持高级用例,例如跨多个目标共享观察者!

现在我们已经了解了一些有关观察者的知识,让我们通过一个简单的以游戏玩法为中心的示例来检查 API

单击以展开...
use bevy::prelude::*;

#[derive(Event)]
struct DealDamage {
    damage: u8,
}

#[derive(Event)]
struct LoseLife {
    life_lost: u8,
}

#[derive(Event)]
struct PlayerDeath;

#[derive(Component)]
struct Player;

#[derive(Component)]
struct Life(u8);

#[derive(Component)]
struct Defense(u8);

#[derive(Component, Deref, DerefMut)]
struct Damage(u8);

#[derive(Component)]
struct Monster;

fn main() {
    App::new()
        .add_systems(Startup, spawn_player)
        .add_systems(Update, attack_player)
        .observe(on_player_death);
}

fn spawn_player(mut commands: Commands) {
    commands
        .spawn((Player, Life(10), Defense(2)))
        .observe(on_damage_taken)
        .observe(on_losing_life);
}

fn attack_player(
    mut commands: Commands,
    monster_query: Query<&Damage, With<Monster>>,
    player_query: Query<Entity, With<Player>>,
) {
    let player_entity = player_query.single();

    for damage in &monster_query {
        commands.trigger_targets(DealDamage { damage: damage.0 }, player_entity);
    }
}

fn on_damage_taken(
    trigger: Trigger<DealDamage>,
    mut commands: Commands,
    query: Query<&Defense>,
) {
    let defense = query.get(trigger.entity()).unwrap();
    let damage = trigger.event().damage;
    let life_lost = damage.saturating_sub(defense.0);
    // Observers can be chained into each other by sending more triggers using commands.
    // This is what makes observers so powerful ... this chain of events is evaluated
    // as a single transaction when the first event is triggered.
    commands.trigger_targets(LoseLife { life_lost }, trigger.entity());
}

fn on_losing_life(
    trigger: Trigger<LoseLife>,
    mut commands: Commands,
    mut life_query: Query<&mut Life>,
    player_query: Query<Entity, With<Player>>,
) {
    let mut life = life_query.get_mut(trigger.entity()).unwrap();
    let life_lost = trigger.event().life_lost;
    life.0 = life.0.saturating_sub(life_lost);

    if life.0 == 0 && player_query.contains(trigger.entity()) {
        commands.trigger(PlayerDeath);
    }
}

fn on_player_death(_trigger: Trigger<PlayerDeath>, mut app_exit: EventWriter<AppExit>) {
    println!("You died. Game over!");
    app_exit.send_default();
}

未来,我们计划使用钩子和观察者来 替换RemovedComponents使我们的层次结构管理更加健壮,创建 bevy_eventlistener 的第一方替代品作为我们 UI 工作的一部分,以及 构建关系。这些都是功能强大、通用的工具:我们迫不及待地想要看到社区用它们创造的疯狂科学!

当你准备好开始时,请查看 component hooksobservers 示例以了解更多 API 细节。

glTF KHR_texture_transform 支持 #

  • 作者:@janhohenheim、@yrns、@Kanabenki
  • PR #11904

GLTF 扩展 KHR_texture_transform 用于在应用纹理之前对其进行变换。通过读取此扩展,Bevy 现在可以支持各种新的工作流程。我们这里想要强调的是能够轻松地将纹理重复一定的次数。这对于创建旨在跨表面平铺的纹理很有用。我们将展示如何使用 Blender 来做到这一点,但相同的原则适用于任何 3D 建模软件。

让我们看一个我们在 Blender 中准备好的示例场景,它被导出为 GLTF 文件并加载到 Bevy 中。我们首先使用 Blender 中最基本的着色器节点设置

Basic shader node setup

结果是在 Bevy 中的以下场景

Scene with stretched textures

哦,不!一切都拉伸了!这是因为我们将 UV 设置为将纹理恰好映射到网格上一次。有一些方法可以解决这个问题,但最方便的方法是添加着色器节点来缩放纹理,使其重复

Repeating shader node setup

Mapping 节点的數據是导出到 KHR_texture_transform 中的。看看红色部分。这些缩放因子决定了纹理在材质中应重复多少次。调整所有纹理的此值会导致更好的渲染结果

Scene with repeated textures

UI 节点边框半径 #

  • 作者:@chompaa、@pablo-lua、@alice-i-cecile、@bushrat011899
  • PR #12500

UI 节点的边框半径一直是 Bevy 的一个长期请求的功能。现在它得到了支持!

要将边框半径应用于 UI 节点,有一个新的组件 BorderRadiusNodeBundleButtonBundle 现在都有一个用于此组件的字段,名为 border_radius

commands.spawn(NodeBundle {
    style: Style {
        width: Val::Px(50.0),
        height: Val::Px(50.0),
        // We need a border to round a border, after all!
        border: UiRect::all(Val::Px(5.0)),
        ..default()
    },
    border_color: BorderColor(Color::BLACK),
    // Apply the radius to all corners. 
    // Optionally, you could use `BorderRadius::all`.
    border_radius: BorderRadius {
        top_left: Val::Px(50.0),
        top_right: Val::Px(50.0),
        bottom_right: Val::Px(50.0),
        bottom_left: Val::Px(50.0),
    },
    ..default()
});

有一个 新示例 展示了这个新的 API,下面可以看到它的截图

rounded_borders example

使用 AnimationGraph 进行动画混合 #

  • 作者:@pcwalton、@rparrett、@james7132
  • PR #11989

从初学者的角度来看,处理动画似乎很简单。定义一系列关键帧,这些关键帧将模型的各个部分转换为与这些姿势相匹配。我们在那里添加了一些插值以使它们之间平滑过渡,用户告诉您何时启动和停止动画。容易!

但现代动画管道(尤其是在 3D 中!)要复杂得多:动画师希望能够平滑地混合和以编程方式动态地更改不同的动画以响应游戏玩法。为了捕捉这种丰富性,业界已经发展出了动画图的概念,它用于将游戏对象的底层 状态机与应该播放的动画以及每个不同状态之间应该发生的过渡联系起来。

一个玩家角色可能正在行走、奔跑、挥舞剑、用剑防御……为了创造出精致的效果,动画师需要能够平滑地在这几项动画之间切换,改变步行循环的速度以匹配地面上的移动速度,甚至同时执行多项动画!

在 Bevy 0.14 中,我们已经实现了 动画组合 RFC,提供了一个低级 API,它为 Bevy 带来了代码驱动和资产驱动的动画混合。

#[derive(Resource)]
struct ExampleAnimationGraph(Handle<AnimationGraph>);

fn programmatic_animation_graph(
    mut commands: Commands,
    asset_server: ResMut<AssetServer>,
    animation_graphs: ResMut<Assets<AnimationGraph>>,
) {
    // Create the nodes.
    let mut animation_graph = AnimationGraph::new();
    let blend_node = animation_graph.add_blend(0.5, animation_graph.root);
    animation_graph.add_clip(
        asset_server.load(GltfAssetLabel::Animation(0).from_asset("models/animated/Fox.glb")),
        1.0,
        animation_graph.root,
    );
    animation_graph.add_clip(
        asset_server.load(GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb")),
        1.0,
        blend_node,
    );
    animation_graph.add_clip(
        asset_server.load(GltfAssetLabel::Animation(2).from_asset("models/animated/Fox.glb")),
        1.0,
        blend_node,
    );

    // Add the graph to our collection of assets.
    let handle = animation_graphs.add(animation_graph);

    // Hold onto the handle
    commands.insert_resource(ExampleAnimationGraph(handle));
}

虽然它今天可以发挥出巨大的作用,但大多数动画师最终会更喜欢使用 GUI 编辑这些图。我们计划在传奇的 Bevy 编辑器中,在这个 API 之上构建一个 GUI。如今,也有一些第三方解决方案,比如 bevy_animation_graph

要了解更多信息并查看资产驱动的方法是什么样的,请查看新的 animation_graph 示例

改进的颜色 API #

颜色是构建良好游戏的重要组成部分:UI、效果、着色器等等都需要功能齐全、正确且便捷的颜色工具。

Bevy 现在支持各种各样的颜色空间,每个颜色空间都有自己的类型(例如 LinearRgbaHslaOklaba),并提供了一系列针对它们的经过充分记录的操作和转换。

新的 API 更加防错、更符合习惯用法,并允许我们通过在渲染内部存储 LinearRgba 类型来节省工作。这个坚实的基础让我们能够实现各种有用的操作,这些操作被聚集成像 HueAlpha 这样的特征,让你可以对任何具有所需属性的颜色空间进行操作。重要的是,现在支持颜色混合/混合:非常适合程序化生成调色板和处理动画。

use bevy_color::prelude::*;

// Each color space now corresponds to a specific type
let red = Srgba::rgb(1., 0., 0.);

// All non-standard color space conversions are done through the shortest path between
// the source and target color spaces to avoid a quadratic explosion of generated code.
// This conversion...
let red = Oklcha::from(red);
// ...is implemented using
let red = Oklcha::from(Oklaba::from(LinearRgba::from(red)));

// We've added the `tailwind` palette colors: perfect for quick-but-pretty prototyping!
// And the existing CSS palette is now actually consistent with the industry standard :p
let blue = tailwind::BLUE_500;

// The color space that you're mixing your colors in has a huge impact!
// Consider using the scientifically-motivated `Oklcha` or `Oklaba` for a perceptually uniform effect.
let purple = red.mix(blue, 0.5);

大多数面向用户的 API 仍然接受与颜色空间无关的 Color(它现在包装了我们的颜色空间类型),而渲染内部使用基于物理的 LinearRgba 类型。有关不同颜色空间的概述以及它们各自的用途,请查看我们的 颜色空间使用 文档。

bevy_color 提供了一个可靠的、类型安全的 foundation,但它才刚刚起步。如果你想要另一个颜色空间,或者你想对你的颜色做更多事情,请打开一个 issue 或 PR,我们很乐意提供帮助!

另请注意,bevy_color 旨在作为独立的 crate 有效地运行:随时在你的非 Bevy 项目中依赖它。

挤压形状 #

Bevy 0.14 引入了一组全新的基元:挤压!

挤压是一个二维基元(基本形状),它通过某种深度挤压到三维空间中。生成的形状是棱柱(或者在圆形的特殊情况下,是圆柱体)。

// Create an ellipse with width 2 and height 1.
let my_ellipse = Ellipse::from_size(2.0, 1.0);

// Create an extrusion of this ellipse with a depth of 1.
let my_extrusion = Extrusion::new(my_ellipse, 1.);

所有挤压都沿 Z 轴挤压。这保证了深度为 0 的挤压与其对应的基本形状是相同的,正如人们所期望的那样。

测量和采样

由于所有具有实现 Measured2d 的基本形状的挤压都实现了 Measured3d,因此你可以轻松地获得挤压的表面积或体积。如果你有一个自定义二维基元的挤压,你只需为你的基元实现 Measured2dMeasured3d 就会自动为挤压实现。

同样,如果你挤压的基本形状实现了 ShapeSample<Output = Vec2>Measured2d,则可以对任何挤压的边界和内部进行采样。

// Create a 2D capsule with radius 1 and length 2, extruded to a depth of 3
let extrusion = Extrusion::new(Capsule2d::new(1.0, 2.0), 3.0);

// Get the volume of the extrusion
let volume = extrusion.volume();

// Get the surface area of the extrusion
let surface_area = extrusion.area();


// Create a random number generator
let mut rng = StdRng::seed_from_u64(4);

// Sample a random point inside the extrusion
let interior_sample = extrusion.sample_interior(&mut rng);

// Sample a random point on the surface of the extrusion
let boundary_sample = extrusion.sample_boundary(&mut rng);

绑定

你还可以获得挤压的包围球体和轴对齐包围盒 (AABB)。如果你有一个实现 Bounded2d 的自定义二维基元,你只需为你的基元实现 BoundedExtrusion)。默认实现将提供最佳结果,但可能比适合你的基元的解决方案速度慢。

网格化

但是挤压并不仅仅存在于数学世界中。它们也可以被网格化并在屏幕上显示!

同样,Bevy 使为你的自定义基元添加网格化支持变得容易!你只需为你的二维基元实现网格化,然后为你的二维基元的 MeshBuilder 实现 Extrudable

在实现 Extrudable 时,你必须提供有关基本形状周边的哪些段应进行平滑着色或平面着色以及哪些顶点属于这些周边段的每个的信息。

a 2D heart primitive and its extrusion

Extrudable 特征允许你轻松地为自定义基元的挤压实现网格化。当然,你也可以为你的挤压手动实现网格化。

如果你想查看此功能的完整实现,你可以查看 自定义基元示例

更多 Gizmo #

  • 作者:@mweatherley、@Kanabenki、@MrGVSV、@solis-lumine-vorago、@alice-i-cecile
  • PR #12211

Bevy 中的 Gizmo 允许开发人员轻松地绘制任意形状以帮助调试或创作内容,但也用于可视化场景的特定属性,例如网格的 AABB。

在 0.14 中,几个新的 Gizmo 已被添加到 bevy::gizmos

圆角盒子 Gizmo

圆角盒子和立方体非常适合可视化区域和碰撞体。

如果你将 corner_radiusedge_radius 设置为正值,则角将向外圆角。但是,如果你提供负值,则角将翻转并向内弯曲。

rounded gizmos cuboids rounded gizmos rectangles

网格 Gizmo

新的网格 Gizmo 类型已通过 Gizmos::grid_2dGizmos::grid 添加,用于在二维或三维空间中绘制平面网格,以及 Gizmos::grid_3d 用于绘制三维网格。

每种网格类型都可以沿其轴倾斜、缩放和细分,并且你可以分别控制要绘制哪些外边缘。

Grid gizmos screenshot

坐标轴 Gizmo

新的 Gizmos::axes 添加了一种简单的方法来显示任何对象的 Transform 以及基本大小的位置、方向和比例。每个轴箭头的尺寸与其在提供的 Transform 中的相应轴比例成正比。

Axes gizmo screenshot

灯光 Gizmo

新的 ShowLightGizmo 组件实现了保留的 gizmo,用于可视化 SpotLightPointLightDirectionalLight 的灯光。大多数灯光属性都通过 gizmo 可视化表示,并且可以设置 gizmo 颜色以匹配灯光实例或使用各种其他行为。

与其他保留的 gizmo 类似,ShowLightGizmo 可以通过 LightGizmoConfigGroup 在每个实例或全局范围内进行配置。

Light gizmos screenshot

Gizmo 线样式和接头 #

以前的 Bevy 版本支持绘制线条 gizmo。

fn draw_gizmos(mut gizmos: Gizmos) {
    gizmos.line_2d(Vec2::ZERO, Vec2::splat(-80.), RED);
}

但是,自定义 gizmo 的唯一方法是更改其颜色,这对于某些用例来说可能有限制。此外,线条条带中两条线的交汇点(其接头)存在小的间隙。

从 Bevy 0.14 开始,您可以更改每个 gizmo 配置组的线条样式及其接头。

fn draw_gizmos(mut gizmos: Gizmos) {
    gizmos.line_2d(Vec2::ZERO, Vec2::splat(-80.), RED);
}

fn setup(mut config_store: ResMut<GizmoConfigStore>) {
    // Get the config for you gizmo config group
    let (config, _) = config_store.config_mut::<DefaultGizmoConfigGroup>();
    // Set the line style and joints for this config group
    config.line_style = GizmoLineStyle::Dotted;
    config.line_joints = GizmoLineJoint::Bevel;
}

新的线条样式可用于 2D 和 3D,并尊重其配置组的 line_perspective 选项。

可用的线条样式包括

  • GizmoLineStyle::Dotted:绘制点线,每个点都是一个正方形。
  • GizmoLineStyle::Solid:绘制实线 - 这是默认行为,也是 Bevy 0.14 之前唯一的可用行为。

new gizmos line styles

类似地,新的线条接头提供各种选项。

  • GizmoLineJoint::Miter,它将两条线都延伸到它们在共同斜接点处相遇为止。
  • GizmoLineJoint::Round(resolution),它将近似一个弧线,填补两条线之间的间隙。resolution 决定用于近似弧线几何形状的三角形数量。
  • GizmoLineJoint::Bevel,它使用直线段连接两条连接线的端点。
  • GizmoLineJoint::None,它不使用任何接头,并留下小的间隙 - 这是默认行为,也是 Bevy 0.14 之前唯一的可用行为。

new gizmos line joints

您可以查看 2D gizmo 示例,它演示了线条样式和接头的使用!

UI 节点轮廓 Gizmo #

  • 作者:@pablo-lua、@nicopap、@alice-i-cecile
  • PR #11237

在网络上使用 UI 时,能够快速调试所有框的大小非常有用。我们现在有一个原生 布局工具,它为所有 节点 添加了 gizmo 轮廓。

启用工具后的示例

Ui example with the overlay tool enabled

use bevy::prelude::*;

// You first have to add the DebugUiPlugin to your app
let mut app = App::new()
    .add_plugins(bevy::dev_tools::ui_debug_overlay::DebugUiPlugin);

// In order to enable the tool at runtime, you can add a system to toggle it
fn toggle_overlay(
    input: Res<ButtonInput<KeyCode>>,
    mut options: ResMut<bevy::dev_tools::ui_debug_overlay::UiDebugOptions>,
) {
    info_once!("The debug outlines are enabled, press Space to turn them on/off");
    if input.just_pressed(KeyCode::Space) {
        // The toggle method will enable the debug_overlay if disabled and disable if enabled
        options.toggle();
    }

}

// And add the system to the app
app.add_systems(Update, toggle_overlay);

上下文清除 Gizmo #

Gizmo 通过立即模式 API 绘制。这意味着您在每次更新时绘制要显示的所有 gizmo,并且只有这些 gizmo 会显示。以前,更新指的是“每次 Main 调度运行时”。这与帧速率相匹配,因此它通常非常有效!但是,当您尝试在 FixedMain 期间绘制 gizmo 时,它们会闪烁或渲染多次。在 Bevy 0.14 中,这现在可以正常工作了!

这可以扩展到与自定义调度一起使用。不再是单个存储,而是有多个 存储,它们通过上下文类型参数区分。您也可以在 Gizmos 系统参数上设置类型参数,以选择要写入的存储。您可以选择何时绘制或清除添加的存储:默认存储(() 上下文)中的任何 gizmo 在 Last 调度期间都会显示。

查询连接 #

ECS 查询现在可以组合,返回包含在两个查询中的实体的数据。

fn helper_function(a: &mut Query<&A>, b: &mut Query<&B>){    
    let a_and_b: QueryLens<(Entity, &A, &B)> = a.join(b);
    assert!(a_and_b.iter().len() <= a.len());
    assert!(a_and_b.iter().len() <= b.len());
}

在大多数情况下,您应该继续简单地在原始查询中添加更多参数。Query<&A, &B> 通常比稍后连接它们更清晰。但是,当复杂的系统或帮助程序函数将您逼入困境时,如果需要,查询连接就在那里。

如果您熟悉数据库术语,这是一种 "内连接"。正在考虑其他类型的查询连接。也许您可以尝试 后续问题

计算状态和子状态 #

  • 作者:@lee-orr、@marcelchampagne、@MiniaczQ、@alice-i-cecile
  • PR #11426

Bevy 的 States 是一种简单但功能强大的抽象,用于管理应用程序的控制流。

但是,随着用户游戏的(以及非游戏应用程序!)复杂程度的提高,它们的局限性变得更加明显。如果我们想捕捉“在菜单中”的概念,但又有不同的状态对应于应该打开哪个子菜单,该怎么办?如果我们想问“游戏是否暂停”这样的问题,而这个问题只有在游戏内才有意义,该怎么办?

找到一个好的抽象来解决这个问题需要 多次 尝试,并进行大量的实验和讨论。

虽然您现有的 States 代码将与以前一样工作,但如果您正在寻找更多表现力,现在还有两种额外的工具可以使用:计算状态子状态

让我们从一个简单的状态声明开始

#[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)]
enum GameState {
    #[default]
    Menu,
    InGame {
        paused: bool
    },
}

添加 pause 字段意味着仅仅检查 GameState::InGame 并不起作用……状态因其值而异,我们可能希望区分游戏暂停或不暂停时运行的游戏系统!

计算状态

虽然我们可以简单地执行 OnEnter(GameState::InGame{paused: true}),但我们需要能够推断“在游戏期间,暂停或不暂停”。为此,我们定义 InGame 计算状态

#[derive(Clone, PartialEq, Eq, Hash, Debug)]
struct InGame;

impl ComputedStates for InGame {
    // Computed states can be calculated from one or many source states.
    type SourceStates = GameState;

    // Now, we define the rule that determines the value of our computed state.
    fn compute(sources: GameState) -> Option<InGame> {
        match sources {
            // We can use pattern matching to express the
            //"I don't care whether or not the game is paused" logic!
            GameState::InGame {..} => Some(InGame),
            _ => None,
        }
    }
}

子状态

相反,子状态应该在您想要通过 NextState 保持对值的控制,但仍将其存在绑定到某个父状态时使用。

#[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)]
// This macro means that `GamePhase` will only exist when we're in the `InGame` computed state.
// The intermediate computed state is helpful for clarity here, but isn't required:
// you can manually `impl SubStates` for more control, multiple parent states and non-default initial value!
#[source(InGame = InGame)]
enum GamePhase {
    #[default]
    Setup,
    Battle,
    Conclusion
}

初始化

初始化我们的状态很容易:只需在 App 上调用适当的方法,所有必需的机制都会为您设置。

App::new()
   .init_state::<GameState>()
   .add_computed_state::<InGame>()
   .add_sub_state::<GamePhase>()

与任何其他状态一样,计算状态和子状态适用于您习惯使用的所有工具:StateNextState 资源、OnEnterOnExitOnTransition 调度以及 in_state 运行条件。请务必访问 两者 示例 以获取更多信息!

唯一的例外是,为了正确性,计算状态不能通过 NextState 更改。相反,它们严格地从其父状态派生;根据提供的 compute 方法,在状态转换期间自动添加、删除和更新。

所有 Bevy 的状态工具现在都位于专门的 bevy_state crate 中,可以通过功能标志进行控制。渴望回到状态堆栈的日子?希望有一种方法可以重新进入状态?所有状态机制依赖于公共 ECS 工具:资源、调度和运行条件,使其易于构建在之上。我们知道状态机很大程度上取决于个人喜好;因此,如果我们的设计不符合您的口味,请考虑利用 Bevy 的模块化特性并编写自己的抽象或使用社区提供的抽象!

状态范围内的实体 #

  • 作者:@MiniaczQ、@alice-i-cecile、@mockersf
  • PR #13649

状态范围内的实体是一种在社区项目中自然出现的模式。Bevy 0.14 已经接受了它!

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
enum GameState {
 #[default]
  Menu,
  InGame,
}

fn spawn_player(mut commands: Commands) {
    commands.spawn((
        // We mark this entity with the `StateScoped` component.
        // When the provided state is exited, the entity will be
        // deleted recursively with all children.
        StateScoped(GameState::InGame)
        SpriteBundle { ... }
    ))
}

App::new()
    .init_state::<GameState>()
    // We need to install the appropriate machinery for the cleanup code
    // to run, once for each state type.
    .enable_state_scoped_entities::<GameState>()
    .add_systems(OnEnter(GameState::InGame), spawn_player);

通过在设置期间将实体生命周期绑定到状态,我们可以大大减少需要编写的清理代码量!

状态标识转换 #

  • 作者:@MiniaczQ、@alice-i-cecile
  • PR #13579

用户有时会要求我们在从一个状态移动到自身时触发退出和进入步骤。虽然这有其用途(刷新是核心思想),但在其他情况下它可能会令人惊讶且不受欢迎。我们找到了一种折衷方案,允许用户在需要时挂接到这种类型的转换。

StateEventTransition 事件现在将包括从一个状态到自身的转换,这也会传播到所有依赖的 ComputedStatesSubStates

由于它是一个利基功能,OnExitOnEnter 调度默认情况下会忽略新的标识转换,但您可以访问新的 custom_transitions 示例以查看如何绕过或更改该行为!

GPU 视锥剔除 #

Bevy 的渲染堆栈通常是 CPU 密集型的:通过将更多工作转移到 GPU 上,我们可以更好地平衡负载并更快地渲染更多闪亮的东西。视锥剔除是一种优化技术,它会自动隐藏位于相机视野(其视锥)之外的物体。在 Bevy 0.14 中,用户可以选择在 GPU 上执行这项工作,具体取决于其项目的性能特征。

有两个新的组件可用于控制视锥剔除:GpuCullingNoCpuCulling。将这些组件的适当组合附加到相机上,就可以了。

commands.spawn((
    Camera3dBundle::default(),
    // Enable GPU frustum culling (does not automatically disable CPU frustum culling).
    GpuCulling,
    // Disable CPU frustum culling.
    NoCpuCulling
));

世界命令队列 #

  • 作者:@james7132、@james-j-obrien
  • PR #11823

当您拥有独占世界访问权限时,使用 Commands 一直很痛苦。创建一个 CommandQueue,从中生成一个 Commands,发送您的命令,然后应用它?这并非最直观的解决方案。

现在,您可以访问 World 自己的命令队列

let mut world = World::new();
let mut commands = world.commands();
commands.spawn(TestComponent);
world.flush_commands();

虽然这不是性能最好的方法(直接将更改应用到世界并跳过间接调用),但此 API 非常适合快速原型设计或轻松测试您的自定义命令。它还在内部用于为组件生命周期钩子和观察者提供支持。

作为奖励,一次性系统现在会在运行时立即应用其命令(和其他延迟的系统参数)!我们已经拥有独占世界访问权限:为什么要引入延迟和微妙的错误?

降低的多线程执行开销 #

Bevy 的多线程系统执行器中最大的开销来源来自 线程上下文切换,即启动和停止线程。每次唤醒线程时,如果线程的缓存很冷,则可能需要多达 30us。最大限度地减少这些切换对于执行器来说是一项重要的优化。在本周期中,我们实施了两项更改,这些更改显示了改进

在每个系统任务结束时运行多线程执行器

系统执行器负责检查系统的依赖项是否已运行,并评估运行条件,然后为该系统运行任务。旧版多线程执行器作为一项单独的任务运行,该任务在每个任务完成后被唤醒。这有时会导致唤醒一个新线程,以便执行器处理系统完成。

通过将其更改为使系统任务尝试在每个系统完成后运行多线程执行器,我们确保多线程执行器始终在已唤醒的线程上运行。这防止了一个上下文切换来源。在实践中,这将每个 Schedule 运行的上下文切换次数减少了 1 到 3 倍,提高了约 30us/调度。当应用程序具有多个调度时,这可能会累积起来!

组合事件更新系统

以前每个事件类型都只有一个“事件更新系统”实例。仅仅使用 `DefaultPlugins`,就会导致 20 多个系统实例。

每个实例运行速度都很快,因此生成系统任务和唤醒线程以运行所有这些系统所产生的开销,占用了 `First` 调度运行时间的绝大部分。因此,将所有这些系统合并为一个系统,可以避免这种开销,并使 `First` 调度运行得更快。在测试中,这使得调度运行时间从 140us 降至 25us。再次强调,这并不是一个 *巨大* 的进步,但我们始终致力于节省每一微秒的时间!

将 `BackgroundColor` 与 `UiImage` 解耦 #

UI 图像现在可以设置纯色背景。

UI image with background color

`BackgroundColor` 组件现在适用于 UI 图像,而不是在图像本身应用颜色色调。您仍然可以通过设置 `UiImage::color` 来应用颜色色调。例如

commands.spawn((
    ImageBundle {
        image: UiImage {
            handle: assets.load("logo.png"),
            color: DARK_RED.into(),
            ..default()
        },
        ..default()
    },
    BackgroundColor(ANTIQUE_WHITE.into()),
    Outline::new(Val::Px(8.0), Val::ZERO, CRIMSON.into()),
));

合并 WinitEvent #

在处理输入时,接收到的事件的精确顺序通常非常重要,即使这些事件类型不同!考虑一个简单的拖放操作。用户何时确切地释放鼠标按钮,相对于他们执行的许多微小移动?正确获取这些细节对于实现响应迅速、精确的用户体验至关重要。

除了现有的独立事件流之外,我们现在还公开了覆盖所有事件的 `WinitEvent` 事件流,可以直接读取和匹配这些事件流,以便在出现这些问题时进行处理。

递归反射注册 #

  • 作者: @MrGVSV, @soqb, @cart, @james7132
  • PR #5781

Bevy 使用 反射 来动态处理数据,例如序列化和反序列化。Bevy 应用有一个 `TypeRegistry` 来跟踪存在的类型。用户可以在初始化应用或插件时注册自定义类型。

#[derive(Reflect)]
struct Data<T> {
  value: T,
}

#[derive(Reflect)]
struct Blob {
  contents: Vec<u8>,
}

app
  .register_type::<Data<Blob>>()
  .register_type::<Blob>()
  .register_type::<Vec<u8>>()

在上面的代码中,`Data<Blob>` 依赖于 `Blob`,而 `Blob` 又依赖于 `Vec<u8>`,这意味着所有三种类型都需要手动注册,即使我们只关心 `Data<Blob>`。

这既繁琐又容易出错,尤其是在这些类型依赖项仅在其他类型上下文中使用时(即,它们不被用作独立类型)。

在 0.14 版本中,任何派生了 `Reflect` 的类型都会自动注册其所有类型依赖项。因此,当我们注册 `Data<Blob>` 时,`Blob` 也会被注册(这会注册 `Vec<u8>`),从而将我们的注册简化为一行代码

app.register_type::<Data<Blob>>()

请注意,现在删除 `Data<Blob>` 的注册也意味着 `Blob` 和 `Vec<u8>` 可能不会被注册,除非它们通过其他方式注册。如果这些类型需要用作独立类型,则应单独注册它们。

`Rot2` 2D 旋转类型 #

  • 作者: @Jondolf, @IQuick143, @tguichaoua
  • PR #11658

您是否曾经想要在 2D 中使用旋转,却因不得不选择四元数或原始 `f32` 而感到沮丧?我们也是!

我们为您添加了一个方便的 `Rot2` 类型,并附带了许多辅助方法。您可以随意替换您编写的辅助类型,并提交针对我们遗漏的任何有用功能的小型 PR。

`Rot2``Dir2` 类型(以前称为 `Direction2d`)的绝佳补充。前者表示一个角度,而后者是一个单位向量。这些类型相似但不可互换,表示形式的选择很大程度上取决于手头的任务。您可以使用 `direction = rotation * Dir2::X` 旋转方向。要恢复旋转,请使用 `Dir2::X::rotation_to(direction)`,在本例中,可以使用辅助方法 `Dir2::rotation_from_x(direction)`。

虽然这些类型尚未在引擎中得到广泛使用,但我们 *确实* 了解您的痛苦,并且正在评估 提案,了解如何使在 2D 中使用变换变得更加直接和愉快。

变换的对齐 API #

**Bevy 0.14** 添加了一个新的 `Transform::align` 函数,它是 `Transform::look_to` 的更通用形式,它允许您指定要用于主轴和副轴的任何局部轴。

这使您能够执行诸如将飞船的前部指向您正在前进的行星,同时保持右翼指向另一艘飞船的方向,或者将飞船的顶部指向正在拉动它的牵引光束的方向,同时前部旋转以匹配较大飞船的方向等操作。

让我们考虑一艘飞船,我们将使用飞船的前部和右翼作为局部轴

before calling Transform::align

// point the local negative-z axis in the global Y axis direction
// point the local x-axis in the global Z axis direction
transform.align(Vec3::NEG_Z, Vec3::Y, Vec3::X, Vec3::Z)

`align` 将移动它以尽可能地匹配所需的位置

after calling Transform::align

请注意,并非所有旋转都可以构造,并且 文档 解释了在这种情况下会发生什么。

形状和方向的随机采样 #

  • 作者: @13ros27, @mweatherley, @lynn-lumen
  • PR #12484

在游戏开发的背景下,访问随机值通常很有用,无论是为了驱动 NPC 的行为、创建效果,还是仅仅为了创造多样性。为了支持这一点,我们在 `bevy_math` 中添加了一些随机采样功能,这些功能受 `rand` 特性控制。这些功能主要是几何性质的,它们有两种形式。

首先,可以从各种数学原语的边界和内部采样随机点:几个原语并排显示,其中从其内部随机采样了点

在代码中,可以通过几种不同的方式执行此操作,使用 `sample_interior`/`sample_boundary` 或 `interior_dist`/`boundary_dist` API

use bevy::math::prelude::*;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;

let sphere = Sphere::new(1.5);

// Instantiate an Rng:
let rng = &mut ChaCha8Rng::seed_from_u64(7355608);

// Using these, sample a random point from the interior of this sphere:
let interior_pt: Vec3 = sphere.sample_interior(rng);
// or from the boundary:
let boundary_pt: Vec3 = sphere.sample_boundary(rng);

// Or, if we want a lot of points, we can use a Distribution instead...
// to sample 100000 random points from the interior:
let interior_pts: Vec<Vec3> = sphere.interior_dist().sample_iter(rng).take(100000).collect();
// or 100000 random points from the boundary:
let boundary_pts: Vec<Vec3> = sphere.boundary_dist().sample_iter(rng).take(100000).collect();

请注意,这些方法显式地需要一个 `Rng` 对象,让您控制随机化策略和种子。

当前支持的形状如下

2D: `Circle`、`Rectangle`、`Triangle2d`、`Annulus`、`Capsule2d`。

3D: `Sphere`、`Cuboid`、`Triangle3d`、`Tetrahedron`、`Cylinder`、`Capsule3d` 以及可采样 2D 形状的挤压(`Extrusion`)。


类似地,方向类型(`Dir2`、`Dir3`、`Dir3A`)和四元数(`Quat`)现在可以使用 `from_rng` 随机构造

use bevy::math::prelude::*;
use rand::{random, Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;

// Instantiate an Rng:
let rng = &mut ChaCha8Rng::seed_from_u64(7355608);

// Get a random direction:
let direction = Dir3::from_rng(rng);

// Similar, but requires left-hand type annotations or inference:
let another_direction: Dir3 = rng.gen();

// Using `random` to grab a value using implicit thread-local rng:
let yet_another_direction: Dir3 = random();

用于分析 GPU 性能的工具 #

虽然 Tracy 已经允许我们测量每个系统的 CPU 时间,但我们的 GPU 诊断要弱得多。在 Bevy 0.14 中,我们通过 `RenderDiagnosticsPlugin` 添加了对两类渲染相关统计数据的支持

  1. **时间戳查询:**GPU 上特定工作部分的运行时间有多长?
  2. **管道统计数据:**有关发送到 GPU 的工作量的信息。

虽然时间戳查询听起来像是终极诊断工具,但它们也有一些注意事项。首先,由于 GPU 会根据工作负载(GPU 工作的空闲间隔,例如,大量连续的屏障或大型分派的尾部)或 GPU 的物理温度动态地提高和降低时钟速度,因此它们的帧间变化很大。要获得准确的测量结果,您需要查看汇总统计数据:平均值、中位数、第 75 个百分位数等。

其次,虽然时间戳查询会告诉您某项操作的运行时间,但它不会告诉您为什么速度慢。要查找瓶颈,您需要使用来自 GPU 供应商的 GPU 分析器(英伟达的 NSight、AMD 的 RGP、英特尔的 GPA 或苹果的 XCode)。这些工具会提供有关缓存命中率、线程组占用率等的更详细的统计数据。另一方面,它们会将 GPU 的时钟锁定到基本速度以获得稳定的结果,因此它们不会为您提供对实际性能的良好指示。

`RenderDiagnosticsPlugin` 跟踪以下管道统计数据,这些数据记录在 Bevy 的 `DiagnosticsStore` 中:已用 CPU 时间、已用 GPU 时间、顶点着色器 调用、片段着色器 调用、计算着色器 调用、裁剪器调用 以及 裁剪器图元

您还可以跟踪各个渲染/计算通道、通道组(例如,所有阴影通道)以及通道中的各个命令(例如,绘制调用)。为此,请使用 `RecordDiagnostics` 特征中的方法对其进行检测。

新的几何原语 #

  • 作者: @vitorfhc, @Chubercik, @andristarr, @spectria-limina, @salvadorcarvalhinho, @aristaeus, @mweatherley
  • PR #12508

几何形状在游戏开发中有着广泛的应用,从渲染简单的项目到屏幕上显示/调试,再到用于碰撞器、物理、射线投射等。

为此,几何形状原语在 Bevy 0.13 中引入,并且该领域的工作一直在继续,Bevy 0.14 带来了 `Triangle3d``Tetrahedron` 3D 原语的添加,以及 `Rhombus``Annulus``Arc2d``CircularSegment` 以及 `CircularSector` 2D 原语。与往常一样,这些原语都有用于查询几何信息(如周长、面积和体积)的方法,并且它们都支持网格化(如果适用)以及小工具显示。

改进 `Point` 并将其重命名为 `VectorSpace` #

  • 作者: @mweatherley, @bushrat011899, @JohnTheCoolingFan, @NthTensor, @IQuick143, @alice-i-cecile
  • PR #12747

线性代数在游戏中无处不在,我们希望确保它易于理解。这就是我们添加新的 `VectorSpace` 特征的原因,这是我们让 `bevy_math` 更加通用、表达性强和数学上合理的一部分工作。任何实现了 `VectorSpace` 的东西都表现得像一个向量。更正式地说,该特征要求实现满足向量加法和标量乘法的向量空间公理。我们还添加了 `NormedVectorSpace` 特征,其中包含用于距离和大小的 API。

这些特性是新的曲线和形状采样 API 的基础。VectorSpace 已针对 f32glam 向量类型和几种新的颜色空间类型实现。它完全取代了 bevy_math::Point

Bevy 中的样条模块长期以来一直缺乏一些功能。样条在游戏开发中非常有用,因此改进它们将改善所有使用它们的组件。

最大的添加是 NURBS 支持!它是 B 样条的变体,具有更多可调整的参数,可以创建特定的曲线形状。我们还添加了一个 LinearSpline,它可以用于在曲线中放置直线段。CubicCurve 现在充当曲线段的序列,你可以向其中添加新的片段,这样你就可以将各种样条类型混合在一起形成一条路径。

二维网格线框 #

线框材质用于渲染网格的单个边和面。它们通常用作可视化几何体的调试工具,但也可以用于各种风格化效果。Bevy 支持将 3D 网格显示为线框,但直到现在才缺乏对 2D 网格执行此操作的能力。

要将你的 2D 网格渲染为线框,请将 Wireframe2dPlugin 添加到你的应用程序中,并将 Wireframe2d 组件添加到你的精灵中。可以通过添加 Wireframe2dColor 组件对每个对象配置线框的颜色,或者通过插入 Wireframe2dConfig 资源来全局配置。

有关如何使用该功能的示例,请查看新的 wireframe_2d 示例

A screenshot demonstrating the new 2D wireframe material

自定义反射字段属性 #

Bevy 的反射系统的一项功能是能够将任意“类型数据”附加到类型。这最常用于允许动态调用特征方法。但是,一些用户将其视为进行其他很棒事情的机会。

惊人的 bevy-inspector-egui 有效地使用类型数据来允许用户为每个字段配置其检查器 UI

use bevy_inspector_egui::prelude::*;
use bevy_reflect::Reflect;

#[derive(Reflect, Default, InspectorOptions)]
#[reflect(InspectorOptions)]
struct Slider {
    #[inspector(min = 0.0, max = 1.0)]
    value: f32,
}

从这一点得到启发,Bevy 0.14 在派生 Reflect 时添加了对自定义属性的适当支持,因此用户和第三方板条箱不再需要为此目的专门创建自定义类型数据。这些属性可以使用 #[reflect(@...)] 语法附加到结构体、枚举、字段和变体,其中 ... 可以是解析为实现 Reflect 的类型的任何表达式。

例如,我们可以使用 Rust 的内置 RangeInclusive 类型为字段指定我们自己的范围

use std::ops::RangeInclusive;
use bevy_reflect::Reflect;

#[derive(Reflect, Default)]
struct Slider {
    #[reflect(@RangeInclusive<f32>::new(0.0, 1.0))]
    // Since this accepts any expression,
    // we could have also used Rust's shorthand syntax:
    // #[reflect(@0.0..=1.0_f32)]
    value: f32,
}

然后可以使用 TypeInfo 动态访问属性

let TypeInfo::Struct(type_info) = Slider::type_info() else {
    panic!("expected struct");
};

let field = type_info.field("value").unwrap();

let range = field.get_attribute::<RangeInclusive<f32>>().unwrap();
assert_eq!(*range, 0.0..=1.0);

此功能为构建在 Bevy 的反射系统之上的事物打开了大量可能性。通过使其独立于任何特定用法,它允许各种用例,包括将来帮助编辑器工作。

事实上,此功能已被 bevy_reactor 用于为其自定义检查器 UI 提供支持

#[derive(Resource, Debug, Reflect, Clone, Default)]
pub struct TestStruct {
    pub selected: bool,

    #[reflect(@ValueRange::<f32>(0.0..1.0))]
    pub scale: f32,

    pub color: Srgba,
    pub position: Vec3,
    pub unlit: Option<bool>,

    #[reflect(@ValueRange::<f32>(0.0..10.0))]
    pub roughness: Option<f32>,

    #[reflect(@Precision(2))]
    pub metalness: Option<f32>,

    #[reflect(@ValueRange::<f32>(0.0..1000.0))]
    pub factors: Vec<f32>,
}

A custom UI inspector built using the code above in bevy_reactor

查询迭代排序 #

Bevy 不对项目的顺序做任何保证。因此,如果我们希望按照特定顺序处理查询项目,则需要对其进行排序!我们可能希望按顺序显示玩家的得分,或者为了网络稳定性确保一致的迭代顺序。在 0.13 中,排序可能如下所示

#[derive(Component, Copy, Clone, Deref)]
pub struct Attack(pub usize)

fn handle_enemies(enemies: Query<(&Health, &Attack, &Defense)>) {
    // An allocation!
    let mut enemies: Vec<_> = enemies.iter().collect();
    enemies.sort_by_key(|(_, atk, ..)| *atk)
    for enemy in enemies {
        work_with(enemy)
    }
}

当在多个系统中进行排序时,这会变得特别繁琐且重复。即使我们始终希望相同的排序,不同的 Query 类型也会使抽象成为用户不合理地困难!为了解决这个问题,我们在 QueryIter 类型上实现了新的排序方法,将示例变为

// To be used as a sort key, `Attack` now implements Ord.
#[derive(Component, Copy, Clone, Deref, PartialEq, Eq, PartialOrd, Ord)]
pub struct Attack(pub usize)

fn handle_enemies(enemies: Query<(&Health, &Attack, &Defense)>) {
    // Still an allocation, but undercover.
    for enemy in enemies.iter().sort::<&Attack>() {
        work_with(enemy)
    }
}

要使用 Attack 组件对我们的查询进行排序,我们将它指定为 sort 的泛型参数。要按多个 Component 进行排序,我们可以这样做,与原始 Query 类型中 Component 的顺序无关:enemies.iter().sort::<(&Defense, &Attack)>()

泛型参数可以被认为是原始查询的 透镜 或“子集”,实际执行底层排序。然后,结果在内部用于返回对原始查询项目进行排序的新查询迭代器。使用默认的 sort,透镜必须完全 Ord,就像 slice::sort 一样。如果这还不够,我们还有来自 slice 的其余 6 种排序方法的对应方法!

泛型透镜参数的工作方式与 Query::transmute_lens 相同。我们不使用过滤器,它们继承自原始查询。 transmute_lens 基础设施有一些不错的附加功能,这使得这一点成为可能

fn handle_enemies(enemies: Query<(&Health, &Attack, &Defense, &Rarity)>) {
    for enemy in enemies.iter().sort_unstable::<Entity>() {
        work_with(enemy)
    }
}

因为我们可以将 Entity 添加到任何透镜,所以我们可以在不将其包含在原始查询中的情况下按它进行排序!

这些排序方法适用于 Query::iterQuery::iter_mut!目前,Query 上的其余迭代器方法不支持排序。排序返回 QuerySortedIter,它本身是一个迭代器,可以对它使用更进一步的迭代器适配器。

请记住,透镜会增加一些开销,因此这些查询迭代器排序的执行效率不如手动排序。但是,这强烈依赖于工作负载,因此如果相关,请自行测试!

SystemBuilder #

Bevy 用户喜欢系统,因此我们为他们的系统创建了一个构建器,这样他们就可以从系统内部构建系统。在运行时,使用动态定义的组件和资源类型!

虽然你可以使用 SystemBuilder 作为 SystemState API 的一个符合人体工程学的选择,将 World 分割成不相交的借用,但它的真正价值在于其动态使用。

你可以选择根据运行时分支创建不同的系统,或者更有趣的是,查询等可以使用运行时定义的组件 ID。这是朝着创建用于处理 动态查询 的符合人体工程学和安全的 API 迈出的又一步,为想要集成脚本语言或为其游戏烘焙复杂修改支持的开发人员奠定了基础。

// Start by creating builder from the world
let system = SystemBuilder::<()>::new(&mut world)
    // Various helper methods exist to add `SystemParam`.
    .resource::<R>()
    .query::<&A>()
    // Alternatively use `.param::<T>()` for any other `SystemParam` types.
    .param::<MyParam>()
    // Finish it all up with a call `.build`
    .build(my_system);
// The parameters the builder is initialized with will appear first in the arguments.
let system = SystemBuilder::<(Res<R>, Query<&A>)>::new(&mut world)
    .param::<MyParam>()
    .build(my_system);
// Parameters like `Query` that implement `BuildableSystemParam` can use
// `.builder::<T>()` to build in place.
let system = SystemBuilder::<()>::new(&mut world)
    .resource::<R>()
    // This turns our query into a `Query<&A, With<B>>`
    .builder::<Query<&A>>(|builder| { builder.with::<B>(); })
    .param::<MyParam>()
    .build(my_system);
world.run_system_once(system);

限制渲染资产 #

  • 作者:@robtfm,@IceSentry,@mockersf
  • PR #12622

使用大量资产?在短时间内将大量字节上传到 GPU 可能会导致卡顿,因为渲染世界正在等待上传完成。

通常,如果应用程序运行流畅,而不是卡顿,体验会更令人愉快,并且在看到资产出现之前延迟几帧通常甚至不会被察觉。

这种体验现在已成为可能

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(RenderAssetBytesPerFrame::new(1_000_000_000)) // Tune to your situation by experimenting!
        .run();
}

就是这样!提供的数字应通过找出无卡顿和可接受延迟之间良好的权衡来选择。

此功能依赖于资产知道在发送到 GPU 时将占用多少字节。目前,图像和网格知道这一点,预计将来会有更多资产类型能够报告这一点。

StandardMaterial UV 通道选择 #

以前,StandardMaterial 始终默认为对除光照贴图之外的每个纹理使用 ATTRIBUTE_UV_0,这对于许多 gltf 文件来说不够灵活。在Bevy 0.14 中,添加了一个新的 UvChannel 枚举,允许你选择在 StandardMaterial 中用于每个纹理的通道。

以下是展示对所有纹理跨越 ATTRIBUTE_UV_1 的支持的之前和之后的示例

UV Channel Selection

删除对 RenderLayers 的限制 #

  • 作者:@tychedelia,@robtfm,@UkoeHB
  • PR #13317

渲染层用于快速切换对象集的可见性,并控制哪些对象可以被哪些摄像机看到。这对于调试视图、装备预览屏幕、可切换的叙事 UI 等非常有用。

在 Bevy 0.14 之前,成员身份由一个位掩码定义,该位掩码的可用插槽有限。现在,你定义的层数不再有任何实际限制,这对于像 nannou 这样的创意编码应用程序特别有用!我们确保将常见情况保持快速,但现在使用可增长的掩码,该掩码将在需要时为其他层分配空间。请记住,每层检查可见性仍然会产生成本,但这允许更动态的用途,其中可以根据需要创建层,而不必担心超出限制。

on_unimplemented 诊断 #

  • 作者:@bushrat011899,@alice-i-cecile,@Themayu
  • PR #13347

Bevy 充分利用了 Rust 提供的强大类型系统,但这种强大的功能通常会带来困惑,即使是微小的错误也会导致混乱。

use bevy::prelude::*;

struct MyResource;

fn main() {
    App::new()
        .insert_resource(MyResource)
        .run();
}

运行以上代码将产生编译器错误,让我们看看为什么...

单击以展开...
error[E0277]: the trait bound `MyResource: Resource` is not satisfied
   --> example.rs:6:32
    |
6   |     App::new().insert_resource(MyResource).run();
    |                --------------- ^^^^^^^^^^ the trait `Resource` is not implemented for `MyResource`
    |                |
    |                required by a bound introduced by this call
    |
    = help: the following other types implement trait `Resource`:
              AccessibilityRequested
              ManageAccessibilityUpdates
              bevy::a11y::Focus
              DiagnosticsStore
              FrameCount
              bevy::prelude::Axis<T>
              WinitActionHandlers
              ButtonInput<T>
            and 127 others
note: required by a bound in `bevy::prelude::App::insert_resource`
   --> /bevy/crates/bevy_app/src/app.rs:537:31
    |
537 |     pub fn insert_resource<R: Resource>(&mut self, resource: R) -> &mut Self {
    |                               ^^^^^^^^ required by this bound in `App::insert_resource`

编译器建议我们使用实现 Resource 的不同类型,或者在 MyResource 上实现该特征。前者对我们毫无帮助,而后者没有提及可用的派生宏。

随着 Rust 1.78 的发布,Bevy 现在可以使用 诊断属性 为编译期间的某些类型的错误提供更直接的消息。

error[E0277]: `MyResource` is not a `Resource`
   --> example.rs:6:32
    |
6   |     App::new().insert_resource(MyResource).run();
    |                --------------- ^^^^^^^^^^ invalid `Resource`
    |                |
    |                required by a bound introduced by this call
    |
    = help: the trait `Resource` is not implemented for `MyResource`
    = note: consider annotating `MyResource` with `#[derive(Resource)]`
    = help: the following other types implement trait `Resource`:
              AccessibilityRequested
...

现在,错误消息有一个更易于理解的入口点,以及一个新的 note 部分,指向资源的派生宏。如果 Bevy 的建议不是你问题的解决方案,则其余的编译器错误仍将包含在内,以防万一。

这些诊断已在 Bevy 的各个特征中实现,我们希望随着 Rust 中添加新功能来改善这种体验。例如,我们真的很想改善使用 Component 元组的体验,但我们还没有做到。你可以在 pull request 和相关的 issue 中了解更多关于此更改的信息。

动画网格的运动向量和 TAA #

早在**Bevy 0.11**中,我们添加了时间抗锯齿 (TAA),它使用运动向量来判断物体移动的速度。然而,在**Bevy 0.11**中,我们只添加了对“静态”网格的运动向量支持,这意味着 TAA 无法用于使用骨骼动画或变形目标的动画网格。

在**Bevy 0.14**中,我们实现了逐物体运动模糊,它也使用运动向量,因此也存在同样的限制。

幸运的是,在**Bevy 0.14**中,我们为带骨骼蒙皮的网格和带变形目标的网格实现了运动向量,弥补了这一差距,并使 TAA、逐物体运动模糊以及未来基于运动向量的功能能够应用于动画网格。

改进矩阵命名 #

游戏引擎通常提供一组矩阵来执行游戏世界中的空间变换。通常,使用以下空间

  • 标准化设备坐标:由图形 API 直接使用
  • 裁剪空间:投影后但在透视除法之前进行的坐标
  • 视点空间:相机视点中的坐标
  • 世界空间:全局坐标(这是我们最常谈论的!)
  • 模型空间:(或局部空间)相对于实体的坐标

一个常见的例子是“模型视图投影矩阵”,它是从模型空间到 NDC 空间的变换(奇怪的是,在这个简写中,视图矩阵通常是从世界到视点空间的变换,但模型矩阵是从模型(或局部)空间到世界空间的变换)。通常,矩阵被称为该简写的一部分,例如,投影矩阵将视图坐标转换为 NDC 坐标。

Bevy 在一些地方有一个视图矩阵,它是从视点到世界空间的变换(而不是从世界到视点空间的变换,如上所示)。此外,即使在一致使用的情况下,单个单词简写也是模棱两可的,可能会造成混淆。我们认为需要一个更清晰的约定。

从现在开始,Bevy 中的矩阵命名为 y_from_x,例如 world_from_local,它表示从局部空间到世界空间坐标的变换。这样做的一个好处是,逆矩阵命名为 x_from_y,当在空间之间进行乘法时,很容易看出它是正确的。

例如,您不再写

let model_view_projection = projection * view * model;

您现在应该写

let clip_from_local = clip_from_view * view_from_world * world_from_local;

类型化的 glTF 标签 #

  • 作者:@mockersf、@rparrett、@alice-i-cecile
  • PR #13586

如果您一直在使用glTF 文件作为您的场景,或者查看过使用 glTF 文件的示例,您可能会在资产路径的末尾看到标签

let model_pine = asset_server.load("models/trees/pine.gltf#Scene0");
let model_hen = asset_server.load("models/animals/hen.gltf#Scene0");
let animation_hen = asset_server.load("models/animals/hen.gltf#Aniamtion1"); // Oh no!

请注意末尾的 #Scene0 语法。glTF 格式能够在一个文件中包含许多东西,包括多个场景、动画、灯光等等。

这些标签是告诉 Bevy 我们要加载文件的哪个部分的方法。

然而,这很容易导致用户错误,并且看起来确实出现了错误!母鸡动画的标签为 Aniamtion1 而不是 Animation1

现在不用担心了!上面代码可以改写如下

let hen = "models/animals/hen.gltf"; // Can re-use this more easily too
let model_pine = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/trees/pine.gltf"));
let model_hen = asset_server.load(GltfAssetLabel::Scene(0).from_asset(hen));
let animation_hen = asset_server.load(GltfAssetLabel::Animation(0).from_asset(hen)); // No typo!

查看glTF 标签文档,了解您可以查询哪些部分。

winit v0.30 #

Winit v0.30 改变了它的 API,以支持基于特性的架构,而不是简单的基于事件的架构。Bevy 0.14 现在实现了这个新的架构,使事件循环处理更容易理解。

现在可以定义一个自定义 winit 用户事件,该事件可以用于触发 App 更新,并且可以在系统内部读取以触发特定行为。这对于从 winit 事件循环之外发送事件并在 Bevy 系统内部管理它们特别有用(请参阅window/custom_user_event.rs 示例)。

UpdateMode 枚举现在只接受两个值:ContinuousReactive。后者公开了 3 个新属性,以支持对设备、用户或窗口事件的响应。之前的 UpdateMode::Reactive 现在等效于 UpdateMode::reactive(),而 UpdateMode::ReactiveLowPower 映射到 UpdateMode::reactive_low_power()

  • Idle:循环尚未开始
  • Running(以前称为 Started):循环正在运行
  • WillSuspend:循环即将暂停
  • Suspended:循环已暂停
  • WillResume:循环即将恢复

注意:Resumed 状态已被删除,因为恢复的应用程序只是 Running

场景、网格和材质 glTF 扩展 #

glTF 3D 模型文件格式允许在extras 属性中传递其他用户定义的元数据,除了基元/节点级别的 glTF 扩展外,Bevy 现在还为以下内容提供了特定的 GltfExtras:

  • 场景:SceneGltfExtras,如果存在,则在场景级别注入
  • 网格:MeshGltfExtras,如果存在,则在网格级别注入
  • 材质:MaterialGltfExtras,如果存在,则在网格级别注入:即如果网格有一个具有 glTF 扩展的材质,则该组件将被注入到那里。

您现在可以轻松地查询这些特定的扩展

fn check_for_gltf_extras(
    gltf_extras_per_entity: Query<(
        Entity,
        Option<&Name>,
        Option<&GltfSceneExtras>,
        Option<&GltfExtras>,
        Option<&GltfMeshExtras>,
        Option<&GltfMaterialExtras>,
    )>,
) {
    // use the extras' data 
    for (id, name, scene_extras, extras, mesh_extras, material_extras) in
        gltf_extras_per_entity.iter()
    {

    }
}

这使得通过 glTF 文件从 Blender 等程序传递信息到 Bevy 更加符合规范,也更加实用!

场景中的资源实体映射 #

Bevy 的 DynamicScene 是一个资源和实体的集合,可以序列化以创建预制件或存档游戏数据之类的集合。当一个 DynamicScene 被反序列化并写入一个 World 中时(例如,当加载存档游戏时),场景内的动态实体标识符必须映射到它们新生成的对应物。

以前,这种映射只对存储在组件上的实体标识符可用。在 Bevy 0.14 中,资源可以反映 MapEntitiesResource 并实现 MapEntities 特性以访问 EntityMapper

    // This resource reflects MapEntitiesResource and implements the MapEntities trait.
    #[derive(Resource, Reflect, Debug)]
    #[reflect(Resource, MapEntitiesResource)]
    struct TestResource {
        entity_a: Entity,
        entity_b: Entity,
    }

    // A simple and common use is a straight mapping of the old entity to the new.
    impl MapEntities for TestResource {
        fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
            self.entity_a = entity_mapper.map_entity(self.entity_a);
            self.entity_b = entity_mapper.map_entity(self.entity_b);
        }
    }

指南针象限和指南针八分圆 #

  • 作者:@BobG1983、@alice-i-cecile
  • PR #13653

在游戏开发中,有很多情况下需要知道给定方向的指南针方位。这在使用四方向或八方向精灵的 2D 游戏中尤其重要,或者想要将模拟输入映射到离散的移动方向。

为了使这更容易,枚举 CompassQuadrant(用于四方向划分)和 CompassOctant(用于八方向划分)已被添加,并实现了 From<Dir2>,方便使用。

加载资产时支持 AsyncSeek #

资产可能很大,而且您并不总是需要单个文件中包含的所有数据。

Bevy 允许您添加您自己的资产加载器。从 Bevy 0.14 开始,您现在可以寻址到您选择的偏移量,读取文件的一部分。

也许您有 .celestial 文件格式,它对宇宙进行编码,但您只想查看始终出现在某个偏移量的微型小行星

#[derive(Default)]
struct UniverseLoader;

#[derive(Asset, TypePath, Debug)]
struct JustALilAsteroid([u8; 128]); // Each lil' asteroid uses this much data

impl AssetLoader for UniverseLoader {
    type Asset = JustALilAsteroid;
    type Settings = ();
    type Error = std::io::Error;
    async fn load<'a>(
        &'a self,
        reader: &'a mut Reader<'_>,
        _settings: &'a (),
        _load_context: &'a mut LoadContext<'_>,
    ) -> Result<JustALilAsteroid, Self::Error> {
        // The universe is big, and our lil' asteroids don't appear until this offset
        // in the celestial file format!
        let offset_of_lil_asteroids = 5_000_000_000_000;

        // Skip vast parts of the universe with the new async seek trait!
        reader
            .seek(SeekFrom::Start(offset_of_lil_asteroids))
            .await?;

        let mut asteroid_buf = [0; 128];
        reader.read_exact(&mut asteroid_buf).await?;

        Ok(JustALilAsteroid(asteroid_buf))
    }

    fn extensions(&self) -> &[&str] {
        &["celestial"]
    }
}

这是可行的,因为 Bevy 的reader 类型(传递给资产加载器的 load 函数)现在实现了AsyncSeek

现实世界中的用例可能是:

  • 您在一个档案中打包了多个资产,并且您希望跳到其中一个资产并读取它
  • 您正在处理大型数据集,例如地图数据,并且您知道在哪里提取某些感兴趣的位置

LoadState::Failed 现在包含错误信息 #

Rust 以其错误处理而自豪,Bevy 也一直在稳步赶上。以前,当使用AssetServer::get_load_state 检查资产是否已加载时,如果出现错误,您只会得到一个无数据的LoadState::Failed。这对于调试来说并不十分有用!

现在,将包含一个完整的AssetLoadError,它具有 14 个不同的变体,可以准确地告诉您哪里出错了。这对于故障排除非常有用,它为更复杂的应用程序中进行适当的错误处理打开了大门。

AppExit 错误 #

在运行应用程序时,可能会有许多原因触发退出。可能是用户按下了退出按钮,或者渲染线程遇到了错误并崩溃了。您可能希望区分这两种情况,并从您的应用程序返回一个适当的退出代码

在**Bevy 0.14**中,您可以这样做。AppExit 事件现在是一个具有两个变体的枚举:SuccessError。错误变体还包含一个非零代码,您可以随意使用它。由于 AppExit 事件现在包含有用的信息,因此应用程序运行器和 App::run 现在将返回导致应用程序退出的事件。

对于插件开发人员,App 已经获得了一个新方法 App::should_exit,它将检查在最近两次更新中是否发送了任何 AppExit 事件。为了确保 AppExit::Success 事件不会掩盖有用的错误信息,即使 AppExit::Success 之后发送了 AppExit::Error 事件,该方法也会返回任何 AppExit::Error 事件。

最后,AppExit 也实现了Termination 特性,因此它可以从 main 返回。

use bevy::prelude::*;

fn exit_with_a_error_code(mut events: EventWriter<AppExit>) {
    events.send(AppExit::from_code(42));
}

fn main() -> AppExit {
    App::new()
        .add_plugins(MinimalPlugins)
        .add_systems(Update, exit_with_a_error_code)
        .run() // There's no semicolon here, `run()` returns `AppExit`.
}

App returning a 42 exit code

将动态链接在 WASM 目标上设为无操作 #

WASM 不支持在运行时链接的动态库。以前,如果您启用了 dynamic_linking 功能,Bevy 将无法编译。

$ cargo build --target wasm32-unknown-unknown --features bevy/dynamic_linking
error: cannot produce dylib for `bevy_dylib v0.13.2` as the target `wasm32-unknown-unknown` does not support these crate types

现在,Bevy 将为所有 WASM 目标回退到静态链接。如果您为开发启用了 dynamic_linking,则不再需要为 WASM 禁用它。

弃用动态插件 #

bevy_dynamic_plugin 是 Bevy 最初的 0.1 版本中添加的一个工具:旨在作为一种工具,用于动态加载/链接 Rust 代码,用于诸如 modding 之类的事情。不幸的是,这个功能没有得到社区的广泛采用,因此多年来几乎没有贡献来改进和记录它。

再加上一个具有挑战性且本质上不安全的 API,导致用户遇到了令人担忧的故障,因此我们决定弃用 bevy_dynamic_plugin,并将在 Bevy 0.15 中将其完全移除。如果您是这个功能的忠实用户,只需将这个相当小的板条箱复制到您自己的项目中,然后像以前一样继续使用。

我们仍然认为 modding 和热重载代码以加快开发时间是 Bevy 应该支持的宝贵用例。我们希望通过将其作为一级板条箱移除,我们可以刺激第三方尝试,并避免用户浪费时间来调查一个复杂的潜在解决方案,然后得出它还无法满足他们的需求的结论。

Bevy 工作组 #

Bevy 有很多才华横溢的贡献者,因此跟踪正在发生的事情并做出明智的决定可能是一个真正的挑战。我们正在尝试工作组:临时小组通过创建设计文档、获得专家的认可,然后进行实现来解决更难的问题。如果您想帮助对 Bevy 进行复杂、高影响力的更改:加入或组建一个工作组!

下一步是什么? #

以上功能可能很棒,但 Bevy 还有哪些正在进行中的功能呢?深入时间迷雾(当你的团队几乎都是志愿者时,预测非常困难!),我们可以看到一些激动人心的工作正在形成

  • 更好的场景:场景是 Bevy 的核心构建块之一:旨在成为创建关卡和创建可重用游戏对象的强大工具,无论它们是单选按钮小部件还是怪物。我们正在开发一个新的场景系统,它使用新的语法,这将使在资产中在代码中定义场景更强大、更令人愉悦。你可以查看(现在有点旧的)项目启动讨论以了解更多信息。我们也即将发布一份设计文档,概述我们的计划和当前实施状态。
  • ECS 关系:关系(将实体链接在一起的一流功能)是人们强烈需要的,但它却非常复杂,推动了我们 ECS 内部机制的功能和重构。该工作组一直在耐心规划我们需要做的事情以及原因,如该RFC中所述。
  • 更好的音频:Bevy 的内置音频解决方案从未真正达到预期效果。该更好的音频工作组正在规划前进的道路。
  • 贡献手册:我们关于如何贡献的文档散布在我们的存储库的各个角落。通过将这些文档收集在一起,该贡献手册工作组希望让发现和维护变得更容易。
  • 曲线抽象:曲线在游戏开发中经常出现,组成该曲线组的数学大师正在设计一个特质来统一和增强它们。
  • 更好的文本:我们现有的文本解决方案无法满足现代 UI 的需求。“Lorem Ipsum”工作组正在研究用更好的解决方案替换它。
  • 关于开发工具的统一视图:在 0.14 中,我们添加了一个存根 bevy_dev_tools crate:一个用于工具和叠加层的地方,可以加速游戏开发,例如性能监控器、飞行相机或用于生成游戏对象的 ingame 命令。我们正在努力添加更多工具,并创建一个开发工具抽象。这将为我们提供一种统一的方式来启用/禁用、自定义和将这组工具分组到工具箱中,从而创建类似于 Quake 控制台或 VSCode 命令面板的东西,其中包含来自生态系统各处的工具。
  • Bevy 远程协议:与正在运行的 Bevy 游戏通信对于构建编辑器、调试器和其他工具来说是一个非常强大的工具。我们正在开发一个基于反射的协议来创建一个能够为整个生态系统提供动力的解决方案。
  • 一个模块化、可维护的渲染图:Bevy 的现有渲染架构在提供可重用渲染器功能方面已经相当不错,例如 RenderPhases、批处理和绘制命令。但是,渲染图接口本身仍然是一个痛点。由于它分布在许多文件中,因此控制流很难理解,而且它大量使用 ECS 资源来传递渲染数据,这实际上不利于模块化。虽然确切的设计尚未最终确定(并且非常欢迎反馈!),但我们一直在积极努力重新设计渲染图,以便朝着模块化和易用性对渲染器进行更大规模的重构。

支持 Bevy #

Bevy 将永远是免费的开源软件,但它的制作并非免费!由于 Bevy 是免费的,我们依赖 Bevy 社区的慷慨捐助来资助我们的工作。如果您是 Bevy 的快乐用户,或者您相信我们的使命,请考虑向 Bevy 基金会捐款... 每一点帮助都至关重要!

捐款 心形图标

贡献者 #

衷心感谢 256 位贡献者,他们使这次发布(以及相关的文档)成为可能!以随机顺序

  • @miroim
  • @vitorfhc
  • @13ros27
  • @andristarr
  • @mrtolkien
  • @atlv24
  • @MrGVSV
  • @SludgePhD
  • @Wuketuke
  • @AndrewDanial
  • @thebluefish
  • @waywardmonkeys
  • @VictorBulba
  • @Earthmark
  • @GitGhillie
  • @andriyDev
  • @shanecelis
  • @mintlu8
  • @bushrat011899
  • @TheCarton
  • @RobWalt
  • @NoahShomette
  • @jake8655
  • @sam-kirby
  • @Olle-Lukowski
  • @R081n
  • @adithramachandran
  • @msvbg
  • @freiksenet
  • @IQuick143
  • @AldanTanneo
  • @ricky26
  • @torsteingrindvik
  • @Chubercik
  • @Themayu
  • @umut-sahin
  • @hut
  • @JMS55
  • @hi-names-nat
  • @BobG1983
  • @gagnus
  • Fpgu
  • @dependabot[bot]
  • @Hexorg
  • @A-Walrus
  • @killercup
  • @rparrett
  • @Zeenobit
  • @Bluefinger
  • @Brezak
  • @nicoburns
  • @lee-orr
  • ebola
  • @JoshuaSchlichting
  • @mrchantey
  • @Aceeri
  • @ramirezmike
  • @mgi388
  • @LuisFigueiredo73
  • @benpinno
  • @james-j-obrien
  • @oli-obk
  • @tguichaoua
  • @SolarLiner
  • @s-puig
  • @dmlary
  • @brandon-reinhart
  • @ekropotin
  • @Victoronz
  • @janhohenheim
  • @Xzihnago
  • @infmagic2047
  • @spooky-th-ghost
  • @greytdepression
  • @philpax
  • @pablo-lua
  • @rmsthebest
  • @mockersf
  • @UkoeHB
  • @eira-fransham
  • @chompaa
  • @ghost
  • @pkupper
  • @porkbrain
  • @tychedelia
  • @Kanabenki
  • @MScottMcBee
  • @gabrielkryss
  • @EmiOnGit
  • @kristoff3r
  • @imrn99
  • @moonlightaria
  • @geekvest
  • @sampettersson
  • @JoJoJet
  • @JeanMertz
  • @arcashka
  • @Testare
  • @Davier
  • @SkiFire13
  • @ArthurBrussee
  • @IceSentry
  • @zuiyu1998
  • @snendev
  • @MiniaczQ
  • @soqb
  • Alice Cecile
  • @Gingeh
  • @Weibye
  • @viridia
  • @MarcoMeijer
  • @lambertsbennett
  • @doonv
  • @DGriffin91
  • @BD103
  • @forgemo
  • @StephenTurley
  • @kaosat-dev
  • @mnmaita
  • @Maximetinu
  • @eidloi
  • @yrns
  • @daxpedda
  • @Kurble
  • @findmyhappy
  • @inodentry
  • @TimJentzsch
  • @alice-i-cecile
  • @eero-lehtinen
  • @OneFourth
  • @pcwalton
  • @MonaMayrhofer
  • @jgayfer
  • @blukai
  • @vertesians
  • @geckoxx
  • @james7132
  • @theredfish
  • @nzhao95
  • @StrikeForceZero
  • @spectria-limina
  • @ChristopherBiscardi
  • @gavlig
  • @Multirious
  • @mweatherley
  • @TheNullicorn
  • @lukaschod
  • @salvadorcarvalhinho
  • @ua-kxie
  • @stinkytoe
  • @Aztro-dev
  • @bcolloran
  • @awwsmm
  • @TheRawMeatball
  • @tomara-x
  • @orzogc
  • @matiqo15
  • @TimLeach635
  • @uwuPyxl
  • @Soulghost
  • @benfrankel
  • @kornelski
  • @paolobarbolini
  • @notmd
  • @mamekoro
  • Franklin
  • @simbleau
  • @NixyJuppie
  • @coolreader18
  • @feyokorenhof
  • @eerii
  • @AmionSky
  • @jkb0o
  • @Shatur
  • @stowmyy
  • @chescock
  • @hymm
  • @ShadowMitia
  • @pietrosophya
  • @jnhyatt
  • @Jondolf
  • @jdm
  • @TrialDragon
  • @Remi-Godin
  • @Friz64
  • @dmyyy
  • @rib
  • @juliohq
  • @stepancheg
  • @nbielans
  • @komadori
  • @djeedai
  • @ManevilleF
  • @mghildiy
  • @BeastLe9enD
  • @JohnTheCoolingFan
  • @kettle11
  • @emilyselwood
  • @cBournhonesque
  • @aristaeus
  • @chrisjuchem
  • @allsey87
  • @nicopap
  • @tjamaan
  • @gibletfeets
  • @floppyhammer
  • @honungsburk
  • @CatThingy
  • @jakobhellermann
  • @aevyrie
  • @jirisvd
  • @CptPotato
  • @NiklasEi
  • @lynn-lumen
  • @AxiomaticSemantics
  • @Azorlogh
  • @zeux
  • @agiletelescope
  • @Elabajaba
  • @RobinKellnerVector
  • @iiYese
  • @ycysdf
  • @Waridley
  • @coreh
  • @yyogo
  • @targrub
  • @marcelchampagne
  • @tygyh
  • @unknownue
  • @TheNeikos
  • @Vrixyz
  • @MTFT-Games
  • @SpecificProtagonist
  • @Zoomulator
  • @afonsolage
  • @NthTensor
  • @cart
  • @maboesanman
  • @IWonderWhatThisAPIDoes
  • @re0312
  • @66OJ66
  • @theon
  • @robtfm
  • @NiseVoid
  • Lukas Chodosevicius
  • @superdump
  • @oyasumi731
  • @ameknite
  • @franklinblanco
  • @bugsweeper
  • @Zeophlite
  • @xStrom
  • @ItsDoot
  • @LeshaInc
  • @maniwani
  • @ickshonpe

完整变更日志 #

上面提到的更改只是我们在这个周期中所做的最吸引人、影响最大的更改。无数的错误修复、文档更改和 API 易用性调整也已加入。有关更改的完整列表,请查看下面列出的 PR。

辅助功能 + 渲染 #

辅助功能 + 窗口 #

动画 #

动画 + 资产 #

动画 + 颜色 + 数学 + 渲染 #

动画 + 颜色 + 渲染 #

动画 + 渲染 #

App #

App + ECS #

资产 #

Assets + Diagnostics #

Assets + ECS #

Assets + Reflection #

Assets + Rendering #

音频 #

构建系统 #

构建系统 + 交叉切面 #

构建系统 + 开发工具 #

构建系统 + ECS + 反射 #

构建系统 + 元数据 #

构建系统 + 渲染 #

颜色 #

颜色 + Gizmos #

颜色 + Gizmos + 渲染 + 文本 + UI #

颜色 + 渲染 #

颜色 + 渲染 + UI #

核心 #

交叉切面 #

交叉切面 + 开发工具 + Gizmos #

交叉切面 + ECS #

开发工具 #

开发工具 + Diagnostics #

开发工具 + Diagnostics + 渲染 #

开发工具 + ECS + 网络 #

开发工具 + Gizmos + UI #

开发工具 + 反射 #

开发者工具 + 窗口 #

诊断 #

诊断 + ECS #

诊断 + 渲染 #

ECS #

ECS + 网络 #

ECS + 网络 + 场景 #

ECS + 指针 #

ECS + 反射 #

ECS + 渲染 #

ECS + 任务 #

ECS + 实用程序 #

编辑器 #

编辑器 + 小部件 #

编辑器 + 反射 #

小部件 #

小部件 + 数学 #

小部件 + 反射 #

小部件 + 渲染 #

层次结构 #

层次结构 + 场景 #

输入 #

输入 + 数学 #

输入 + 窗口 #

数学 #

数学 + 反射 #

数学 + 渲染 #

数学 + 变换 #

数学 + 工具 #

数学 + 窗口 #

元数据 #

修改 #

指针 #

反射 #

反射 + 渲染 #

反射 + 场景 #

反射 + 时间 #

反射 + 工具 #

渲染 #

渲染 + 变换 #

渲染 + UI #

渲染 + 工具 #

渲染 + 窗口 #

场景 #

任务 #

任务 + 窗口 #

文本 #

时间 #

变换 #

变换 + UI #

UI #

工具 #

窗口 #

没有区域标签#