非官方 Bevy 作弊书04-06

发布时间:2024年01月23日

Common Pitfalls - Unofficial Bevy Cheat Book

用 有道 翻译


4 常见陷阱

本章介绍了您在与 Bevy 合作时可能会遇到的一些常见问题或意外情况,并提供了有关如何解决这些问题的具体建议。


Strange compile errors from Bevy or dependencies - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

4.1 奇怪的构建错误

有时,在尝试编译项目时,您可能会遇到奇怪且令人困惑的构建错误。

更新你的 Rust

首先,确保您的 Rust 是最新的。使用 Bevy 时,您必须至少使用 Rust 的最新稳定版本(或 nightly)。

如果您用rustup管理 Rust 安装,您可以运行:

rustup update

清除cargo state状态

许多类型的构建错误通常可以通过强制cargo重新生成其内部状态(重新计算依赖项等)来修复。您可以通过删除Cargo.lock文件和target目录来完成此操作。

rm -rf target Cargo.lock

执行此操作后再次尝试构建您的项目。这些神秘的错误很可能会消失。

此技巧通常可以修复损坏的构建,但如果它对您没有帮助,您的问题可能需要进一步调查。通过 GitHub 或Discord联系 Bevy 社区并寻求帮助。

如果您使用的是最新的 Bevy(“main”),并且上述方法不能解决问题,则您的错误可能是由第 3 方插件引起的。请参阅此页面以获取解决方案。

Cargo Resolver解析器

Cargo 最近添加了一种新的依赖解析器算法,该算法与旧算法不兼容。Bevy需要新的解析器。

如果您只是创建一个新的空白 Cargo 项目,请不要担心。这应该已经由 cargo new正确设置。

如果您从 Bevy 依赖项中收到奇怪的编译器错误,请继续阅读。确保您的配置正确,然后清除cargo state.

单箱项目?Single-Crate Projects

在单箱项目中(如果您的项目中只有一个Cargo.toml文件),如果您使用的是最新的 Rust2021 版本,则会自动启用新的解析器。

因此,您需要在您的Cargo.toml

[package]
edition = "2021"

或者

[package]
resolver = "2"

多箱工作区?Multi-Crate Workspaces?

在多箱 Cargo 工作区中,解析器是整个工作区的全局设置。默认情况下不会启用它。

如果您要将单箱项目转换为工作区,这可能会困扰您。

必须手动将其添加到Cargo 工作区的顶层Cargo.toml中

[workspace]
resolver = "2"

非官方 Bevy 作弊书

4.2 性能缓慢的表现

未优化的调试版本

您可以在调试/开发模式下部分启用编译器优化!

您可以对依赖项(包括 Bevy)启用更高的优化,但不能对您自己的代码启用,以保持快速重新编译!

Cargo.toml.cargo/config.toml

# Enable max optimizations for dependencies, but not for our code:
# 为依赖项启用最大优化,但不包括我们的代码:
[profile.dev.package."*"]
opt-level = 3

以上这些就足以让bevy跑得飞快了。它只会减慢干净构建的速度,而不会影响项目的重新编译时间。

如果您自己的代码执行 CPU 密集型工作,您可能还需要为其启用一些优化。但是,这可能会极大地影响某些项目的编译时间(类似于完整的发布版本),因此通常不建议这样做。

# Enable only a small amount of optimization in debug mode
# 在调试模式下只进行少量优化
[profile.dev]
opt-level = 1

警告!如果您使用调试器(例如gdblldb)来单步调试代码,任何数量的编译器优化都会扰乱体验。您的断点可能会被跳过,并且代码流可能会以意想不到的方式跳转。如果您想调试/单步调试代码,您可能需要?opt-level = 0.

为什么这是必要的?

没有编译器优化的 Rust非常慢。特别是对于 Bevy,默认的cargo build调试设置将导致糟糕的运行时性能。资源加载缓慢且 FPS 较低。

常见症状:

  • 从 GLTF 文件加载具有大量大型纹理的高分辨率 3D 模型可能需要几分钟!这可能会让您误以为您的代码不起作用,因为在它准备好之前您不会在屏幕上看到任何内容。
  • 即使生成几个 2D 精灵或 3D 模型后,帧速率也可能会下降到无法播放的水平。

为什么不使用--release?

您可能听过这样的建议:只要运行--release!然而,这是一个坏建议。不要这样做。

发布模式还禁用“调试断言”:在开发过程中进行额外的检查很有用。许多库还在该设置下包含其他内容。在 Bevy 和 WGPU 中,包括对着色器和 GPU API 使用情况的验证。发布模式会禁用这些检查,从而导致信息量较少的崩溃、热重载问题或潜在的错误/无效逻辑被忽视。

发布模式也会使增量重新编译变慢。这会影响 Bevy 的快速编译时间,并且在您开发时可能会非常烦人。


根据本页顶部的建议,您无需使用 ?--release进行构建,只需以足够的性能测试您的游戏即可。您可以将其用于发送给用户的实际发布版本。

如果需要,您还可以为实际的发布版本启用 LTO(链接时优化),以非常慢的编译时间为代价来挤出更多性能:

[profile.release]
lto = "thin"

Error adding function as system - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

4.3 模糊的 Rust 编译器错误

当您尝试将系统添加 到 Bevy应用程序时,您可能会遇到令人困惑的编译器错误。

初学者常见错误

  • 使用commands: &mut Commands代替mut commands: Commands.
  • 使用Query<MyStuff>代替Query<&MyStuff>Query<&mut MyStuff>
  • 使用Query<&ComponentA, &ComponentB>代替Query<(&ComponentA, &ComponentB)>?(忘记元组)
  • 直接使用您的资源类型,而不使用ResResMut
  • 直接使用您的组件类型,而不将它们放入Query.
  • 在查询中使用捆绑类型。您需要单独的组件。
  • 在函数中使用其他任意类型。

请注意,Query<Entity>是正确的,因为实体 ID 很特殊;它不是一个组件。

将功能添加为系统时出错

错误可能如下所示:

error[E0277]: the trait bound `for<'a, 'b, 'c> fn(...) {system}: IntoSystem<(), (), _>` is not satisfied
   --> src/main.rs:5:21
    |
5   |         .add_system(my_system)
    |          ---------- ^^^^^^^^^ the trait `IntoSystem<(), (), _>` is not implemented for fn item `for<'a, 'b, 'c> fn(...) {system}`
    |          |
    |          required by a bound introduced by this call
    |
    = help: the following other types implement trait `IntoSystemConfigs<Marker>`:
    = ...

该错误(令人困惑地)指向代码中尝试添加系统的位置,但实际上,问题实际上出fn函数定义中!

这是由于您的函数具有无效参数造成的Bevy 只能接受特殊类型作为系统参数!

格式错误的查询出错

您还可能会出现如下错误:

error[E0277]: the trait bound `Transform: WorldQuery` is not satisfied
   --> src/main.rs:10:12
    |
10  |     query: Query<Transform>,
    |            ^^^^^^^^^^^^^^^ the trait `WorldQuery` is not implemented for `Transform`
    |
    = help: the following other types implement trait `WorldQuery`:
              &'__w mut T
              &T
              ()
              (F0, F1)
              (F0, F1, F2)
              (F0, F1, F2, F3)
              (F0, F1, F2, F3, F4)
              (F0, F1, F2, F3, F4, F5)
            and 54 others
note: required by a bound in `bevy::prelude::Query`
   --> ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_ecs-0.10.0/src/system/query.rs:276:37
    |
276 | pub struct Query<'world, 'state, Q: WorldQuery, F: ReadOnlyWorldQuery = ()> {
    |                                     ^^^^^^^^^^ required by this bound in `Query`

要访问您的组件,您需要使用引用语法(&?或?&mut)。

error[E0107]: struct takes at most 2 generic arguments but 3 generic arguments were supplied
   --> src/main.rs:10:12
    |
10  |     query: Query<&Transform, &Camera, &GlobalTransform>,
    |            ^^^^^                      ---------------- help: remove this generic argument
    |            |
    |            expected at most 2 generic arguments
    |
note: struct defined here, with at most 2 generic parameters: `Q`, `F`
   --> ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_ecs-0.10.0/src/system/query.rs:276:12
    |
276 | pub struct Query<'world, 'state, Q: WorldQuery, F: ReadOnlyWorldQuery = ()> {
    |            ^^^^^                 -              --------------------------

当你想要查询多个组件时,你需要将它们放在一个元组中:?(&Transform, &Camera, &GlobalTransform)


https://bevy-cheatbook.github.io/pitfalls/3d-not-rendering.html

非官方 Bevy 作弊书

4.4 3D 对象不显示

本页将列出如果您尝试生成 3D 对象但在屏幕上看不到它时可能遇到的一些常见问题。

父级缺少可见性组件

如果您的实体位于层次结构中,则其所有父级都需要是具有?可见性的组件。即使这些父实体不应该渲染任何内容,它也是必需的。

通过插入VisibilityBundle修复它:

commands.entity(parent)
? ? .insert(VisibilityBundle::default());

或者更好的是,首先确保正确生成父实体。如果您不使用已包含这些组件的捆绑包,则可以使用VisibilityBundleor?SpatialBundle(与Transforms一起使用)。

离相机太远

如果某个物体距离相机超过一定距离,它将被剔除(不渲染)。默认值是1000.0单位。

您可以使用以下far字段 来控制PerspectiveProjection

commands.spawn(Camera3dBundle {
    projection: Projection::Perspective(PerspectiveProjection {
        far: 10000.0, 
        // change the maximum render distance
        // 更改最大渲染距离
        ..default()
    }),
    ..default()
});

缺少顶点属性

确保Mesh包含着色器/材质所需的所有顶点属性。

Bevy 的默认 PBR?StandardMaterial?要求所有网格体具有:

  • 位置
  • 法线

可能需要的其他一些:

  • UV(如果在材质中使用纹理)
  • 切线(仅当使用法线贴图时,否则不需要)

如果您要生成自己的网格数据,请确保提供您需要的一切。

如果您要从资源文件加载网格体,请确保它们包含所需的所有内容(检查您的导出设置)。

如果您需要法线贴图的切线,建议您将它们包含在 GLTF 文件中。这避免了 Bevy 必须在运行时自动生成它们。许多 3D 编辑器(如 Blender)默认不启用此选项。

Bevy GLTF 资产的错误使用

请参阅GLTF 页面,了解如何正确地将 GLTF 与 Bevy 结合使用。

GLTF 文件很复杂。它们包含许多子资产,由不同的 Bevy 类型表示。确保您使用的是正确的东西。

确保您正在生成 GLTF 场景,或者在使用正确的场景?MeshStandardMaterial?,与正确的 GLTF 基元关联。

如果使用的是资源路径,请确保包含所需子资源的标签:

let handle_scene: Handle<Scene> = asset_server.load("my.gltf#Scene0");

如果您生成顶级Gltf?主资产,它将无效。Gltf

如果您生成 GLTF 网格,它将无效。

不支持 GLTF

Bevy 并不完全支持 GLTF 格式的所有功能,并且对数据有一些特定要求。并非所有 GLTF 文件都可以在 Bevy 中加载和渲染。不幸的是,在许多情况下,您不会收到任何错误或诊断消息。

常见的限制:

  • 无法加载嵌入在 ascii ( *.gltf) 文件(base64 编码)中的纹理。将纹理放入外部文件中,或使用二进制 (?*.glb) 格式。
  • 仅当纹理文件(KTX2 或 DDS 格式)包含 Mipmap 时才支持Mipmap。GLTF 规范要求游戏引擎生成缺失的 mipmap 数据,但 Bevy 尚不支持这一点。如果您的资源缺少 mipmap,纹理会看起来有颗粒感/有噪音。

此列表并不详尽。可能还有其他我不知道或忘记包含在此处的不受支持的场景。:)

顶点顺序和剔除

默认情况下,Bevy 渲染器采用逆时针顶点顺序启用背面剔除

如果您从代码生成Mesh,请确保顶点的顺序正确

未优化/调试版本

也许您的资源需要一段时间才能加载?如果没有编译器优化,Bevy 会非常慢。实际上,具有大纹理的复杂 GLTF 文件可能需要一分钟多的时间才能加载并显示在屏幕上。在优化构建中几乎是即时的。看这里


Borrow multiple fields from struct - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

4.5 从结构体中借用多个字段

当您有一个组件资源(即具有多个字段的较大结构)时,有时您希望同时借用多个字段(可能是可变的)。

struct MyThing {
    a: Foo,
    b: Bar,
}

fn my_system(mut q: Query<&mut MyThing>) {
    for thing in q.iter_mut() {
        helper_func(&thing.a, &mut thing.b); // ERROR!
    }
}

fn helper_func(foo: &Foo, bar: &mut Bar) {
    // do something
}

这可能会导致有关借用冲突的编译器错误:

error[E0502]: cannot borrow `thing` as mutable because it is also borrowed as immutable
    |
    |         helper_func(&thing.a, &mut thing.b); // ERROR!
    |         -----------  -----         ^^^^^ mutable borrow occurs here
    |         |            |
    |         |            immutable borrow occurs here
    |         immutable borrow later used by call

解决方案是使用“rebo??rrow”习惯用法,这是 Rust 编程中常见但不明显的技巧

// add this at the start of the for loop, before using `thing`:
// 在for循环的开头,在使用' thing '之前添加以下代码:
let thing = &mut *thing;

// or, alternatively, Bevy provides a method, which does the same:
// 或者,Bevy提供了一个方法,做同样的事情:
let thing = thing.into_inner();

请注意,该行会触发更改检测。即使您之后不修改数据,组件也会被标记为已更改。

解释

Bevy 通常允许您通过特殊的包装类型(例如 [?Res<T>][bevy::Res]、[?ResMut<T>][bevy::ResMut] 和 [?Mut<T>][bevy::Mut](可变地查询组件时))来访问数据。这让 Bevy 可以跟踪对数据的访问。

这些是“智能指针”类型,它们使用 Rust?Deref?特征来取消对数据的引用。它们通常无缝工作,您甚至不会注意到它们。

然而,从某种意义上说,它们对编译器来说是不透明的。当您可以直接访问结构体时,Rust 语言允许单独借用结构体的字段,但是当它包装在另一种类型中时,这不起作用。

上面显示的“重新借用”技巧,有效地将包装器转换为常规 Rust 引用。*thing通过取消引用包装器?DerefMut?,然后&mut可变地借用它。您现在拥有?&mut MyStuff而不是Mut<MyStuff>.


https://bevy-cheatbook.github.io/pitfalls/time.html

非官方 Bevy 作弊书

4.6 Bevy 时间 还是 Rust/OS 时间

不要std::time::Instant::now()获取当前时间。 要从 Bevy 获取您的计时信息,使用??Res<Time>

Rust(和操作系统)为您提供调用该函数时的精确时间。然而,这不是您想要的。

您的游戏系统由 Bevy 的并行调度程序运行,这意味着它们可以在每一帧截然不同的时刻被调用!这将导致时间不一致/紧张,并使您的游戏行为异常或看起来卡顿。

Bevy的 Time为您提供在整个帧更新周期中保持一致的计时信息。它旨在用于游戏逻辑。

这并不是 Bevy 特有的,而是普遍适用于游戏开发。始终从游戏引擎中获取时间,而不是从编程语言或操作系统中获取时间。


Textures/Images are flipped - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

4.7 bevy中的 UV 坐标

在 Bevy 中,纹理/图像像素的垂直轴,以及在着色器中采样纹理时,从上到下指向下方原点位于左上角

这与Bevy 中其他地方使用的世界坐标系不一致,其中 Y 轴指向上方

然而,它与大多数图像文件格式存储像素数据的方式以及大多数图形 API 的工作方式一致(包括 DirectX、Vulkan、Metal、WebGPU,但不包括OpenGL)。

OpenGL(以及基于它的框架)是不同的。如果您以前有过这样的经验,您可能会发现您的纹理看起来垂直翻转。


如果您使用网格,请确保它具有正确的 UV 值。如果它是使用其他软件创建的,请务必选择正确的设置。

如果您正在编写自定义着色器,请确保您的 UV 算法正确。

精灵

如果 2D 精灵的图像被翻转(无论出于何种原因),您可以使用 Bevy 的精灵翻转功能进行纠正:

commands.spawn(SpriteBundle {
    sprite: Sprite {
        flip_y: true,
        flip_x: false,
        ..Default::default()
    },
    ..Default::default()
});

四边形

如果要在Quad?网格上显示图像(或自定义着色器),可以将其垂直翻转,如下所示:

let size = Vec2::new(2.0, 3.0);
let my_quad = shape::Quad::flipped(-size);

(这个解决方法是必要的,因为Bevy原语的flipped功能?Quad只做水平翻转,但我们想要垂直翻转)


Game Engine Fundamentals - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

5 游戏引擎基础知识

本章介绍了使用 Bevy 作为游戏引擎的基础知识。

您应该总体熟悉 Bevy 编程。为此,请参阅Bevy 编程框架章节。

本章涵盖的主题适用于所有希望将 Bevy 用作 ECS 库之外的项目。如果您正在使用 Bevy 制作游戏或其他应用程序,那么这适合您。

本章仅涵盖一般基础知识。值得更广泛讨论的复杂主题在书中都有自己的章节:


Coordinate System - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

5.1 坐标系

2D 、 3D 场景和摄像机

Bevy 在游戏世界中使用右手 Y 向上坐标系。为了保持一致性,3D 和 2D 的坐标系相同。

用 2D 来解释最简单:

  • X 轴从左向右(+X 指向右侧)。
  • Y 轴从下到上(+Y 指向上)。
  • Z 轴从远到近(+Z 指向您,在屏幕外)。
  • 对于 2D,默认情况下原点 (X=0.0;Y=0.0) 位于屏幕中心。

当您使用 2D 精灵时,您可以将背景放在 Z=0.0 上,并将其他精灵放置在增加的正 Z 坐标处,以将它们分层在顶部。

在 3D 中,轴的方向相同:

  • Y 点向上
  • 前进方向为-Z

这是右手坐标系。您可以使用右手的手指来可视化 3 个轴:拇指=X、食指=Y、中=Z。

它与Godot、Maya、OpenGL 相同。与Unity相比,Z轴是倒置的。

比较不同游戏引擎和 3D 软件中坐标系方向的图表?

(图形经许可修改和使用;原创作者:@FreyaHolmer

用户界面

对于 UI,Bevy 遵循与大多数其他 UI 工具包、Web 等相同的约定。

  • 原点位于屏幕左上角
  • Y轴指向下方
  • X 从 0.0(屏幕左边缘)到屏幕像素数(屏幕右边缘)
  • Y 从 0.0(屏幕顶部边缘)到屏幕像素数(屏幕底部边缘)

这些单位表示逻辑(针对 DPI 缩放进行补偿)屏幕像素。

UI 布局从上到下流动,类似于网页。

光标和屏幕

如上所述,光标位置和任何其他窗口(屏幕空间)坐标遵循与 UI 相同的约定。


Transforms - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

5.2 变换

相关官方示例:?transform、?translation、?rotation、?3d_rotation、?scale、?move_sprite、?parenting、任何生成 2D 或 3D 对象的内容。


首先,如果您是游戏开发新手,请快速定义一下:

变换允许您将对象放置在游戏世界中。它是对象的“平移”(位置/坐标)、“旋转”和“缩放”(大小调整)的组合。

您可以通过修改平移来移动对象,通过修改旋转来旋转对象,并通过修改比例来使对象变大或变小。

 
    
// To simply position something at specific coordinates
// 简单地将某些东西定位到特定的坐标
let xf_pos567 = Transform::from_xyz(5.0, 6.0, 7.0);

// To scale an object, making it twice as big in all dimensions
// 缩放对象,使其在所有维度上都是原来的两倍大
let xf_scale = Transform::from_scale(Vec3::splat(2.0));

// To rotate an object in 2D (Z-axis rotation) by 30°
// 在2D (z轴旋转)中旋转对象30°
// (angles are in radians! must convert from degrees!)
// (角度是以弧度表示的!必须从度转换!)
let xf_rot2d = Transform::from_rotation(Quat::from_rotation_z((30.0_f32).to_radians()));

// 3D rotations can be complicated; explore the methods available on `Quat`
// 3D旋转可能很复杂;探索`Quat`上可用的方法

// Simple 3D rotation by Euler-angles (X, Y, Z)
// 简单的三维欧拉角旋转(X, Y, Z)
let xf_rot2d = Transform::from_rotation(Quat::from_euler(
    EulerRot::XYZ,
    (20.0_f32).to_radians(),
    (10.0_f32).to_radians(),
    (30.0_f32).to_radians(),
));

// Everything:
// 所有的:
let xf = Transform::from_xyz(1.0, 2.0, 3.0)
    .with_scale(Vec3::new(0.5, 0.5, 1.0))
    .with_rotation(Quat::from_rotation_y(0.125 * std::f32::consts::PI));

变换组件

在 Bevy 中,变换由两个?组件表示:?TransformGlobalTransform

任何代表游戏世界中对象的实体都需要两者兼而有之。Bevy 的所有内置捆绑类型都 包含它们。

如果您在不使用这些捆绑包的情况下创建自定义实体,则可以使用以下方法之一来确保您不会错过它们:

fn spawn_special_entity(
    mut commands: Commands,
) {
    // create an entity that does not use one of the common Bevy bundles,
    // 创建一个不使用普通Bevy bundle的实体,
    // but still needs transforms and visibility
    // 但仍然需要转换和可见性
    commands.spawn((
        ComponentA,
        ComponentB,
        SpatialBundle {
            transform: Transform::from_scale(Vec3::splat(3.0)),
            visibility: Visibility::Hidden,
            ..Default::default()
        },
    ));
}

Transform

Transform是您通常使用的内容。它包含struct平移、旋转和缩放。要读取或操作这些值,请使用?查询从您的系统访问它。

如果实体有父级,则该Transform?组件是相对于父级的。这意味着子对象将与父对象一起移动/旋转/缩放。

fn inflate_balloons(
    mut query: Query<&mut Transform, With<Balloon>>,
    keyboard: Res<Input<KeyCode>>,
) {
    // every time the Spacebar is pressed,
    // 每次按空格键时,
    // make all the balloons in the game bigger by 25%
    // 让游戏中的所有气球变大25%
    if keyboard.just_pressed(KeyCode::Space) {
        for mut transform in &mut query {
            transform.scale *= 1.25;
        }
    }
}

fn throwable_fly(
    time: Res<Time>,
    mut query: Query<&mut Transform, With<ThrowableProjectile>>,
) {
    // every frame, make our projectiles fly across the screen and spin
    // 每一帧,让我们的投射物在屏幕上飞行并旋转
    for mut transform in &mut query {
        // do not forget to multiply by the time delta!
        // 别忘了乘以时间delta!
        // this is required to move at the same speed regardless of frame rate!
        // 无论帧速率如何,它都要求以相同的速度移动!
        transform.translation.x += 100.0 * time.delta_seconds();
        transform.rotate_z(2.0 * time.delta_seconds());
    }
}

GlobalTransform

GlobalTransform代表着在世界上的绝对全球地位。

如果该实体没有父级,则这将匹配Transform

GlobalTransform的值由 Bevy在内部计算/管理?(?“变换传播”?)

Transform不同的是,平移/旋转/缩放不能直接访问。数据以优化的方式存储(使用Affine3A),并且可以在层次结构中进行无法表示为简单转换的复杂转换。例如,跨多个父级的旋转和缩放的组合会导致剪切。

如果您想尝试将GlobalTransformback 转换为可行的平移/旋转/缩放表示,您可以尝试以下方法:

  • .translation()
  • .to_scale_rotation_translation()(可能无效)
  • .compute_transform()(可能无效)

变换传播

这两个组件由一组内部系统(“变换传播系统”)同步,该系统按时间表
PostUpdate?
运行

注意:当您改变Transform? ?时,?GlobalTransform不会立即更新。在变换传播系统运行之前,它们将不同步。

如果您需要直接使用GlobalTransform?,您应该将您的?系统添加PostUpdate计划中并在 后订购
?TransformSystem::TransformPropagate之后对其进行排序。。

/// Print the up-to-date global coordinates of the player
/// 打印玩家最新的全局坐标
fn debug_globaltransform(
    query: Query<&GlobalTransform, With<Player>>,
) {
    let gxf = query.single();
    debug!("Player at: {:?}", gxf.translation());
}
// the label to use for ordering
// 用于排序的标签
use bevy::transform::TransformSystem;

app.add_systems(PostUpdate,
    debug_globaltransform
        // we want to read the GlobalTransform after
?       // 之后我们要读取GlobalTransform
        // it has been updated by Bevy for this frame
?       // 这个帧已经被Bevy更新了
        .after(TransformSystem::TransformPropagate)
);

TransformHelper

如果您需要在转换传播之前运行的系统GlobalTransform?中获取最新的GlobalTransform信息,则可以使用特殊的TransformHelper?系统参数。TransformHelper?

GlobalTransform它允许您根据需要立即计算特定实体 。

这可能有用的一个例子是让相机跟随屏幕上的实体的系统。您需要更新相机的?Transform(这意味着您必须在 Bevy 的变换传播之前执行此操作,以便它可以考虑相机的新变换),但您还需要知道您正在跟踪的实体的当前最新位置。

fn camera_look_follow(
    q_target: Query<Entity, With<MySpecialMarker>>,
    mut transform_params: ParamSet<(
        TransformHelper,
        Query<&mut Transform, With<MyGameCamera>>,
    )>,
) {
    // get the Entity ID we want to target
?   // 获取我们想要定位的实体ID
    let e_target = q_target.single();
    // compute its actual current GlobalTransform
?   // 计算它实际的当前GlobalTransform
    // (could be Err if entity doesn't have transforms)
?   // (如果实体没有变形,可能会出错)
    let Ok(global) = transform_params.p0().compute_global_transform(e_target) else {
        return;
    };
    // get camera transform and make it look at the global translation
?   // 获取相机变换并使其查看全局转换
    transform_params.p1().single_mut().look_at(global.translation(), Vec3::Y);
}

在内部,TransformHelper其行为类似于两个只读查询。它需要访问Parent?和Transform组件才能完成其工作。它会与我们的其他&mut Transform查询冲突。这就是为什么我们必须?在上面的示例中使用参数集。

注意:如果过度使用TransformHelper,可能会成为性能问题。它为您计算全局变换,但不会更新存储在实体的?GlobalTransform中存储的数据GlobalTransform.?Bevy 稍后仍将在变换传播期间再次执行相同的计算。它导致重复性工作。如果您的系统可以在变换传播后运行,因此它可以在 Bevy 更新后读取值,那么您应该更愿意这样做,而不是使用TransformHelper.


Visibility - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

5.3 能见度

相关官方例子:?parenting.


可见性用于控制是否渲染某些内容。如果你想让一个实体存在于世界中,只是不被显示,你可以隐藏它。

/// Prepare the game map, but do not display it until later
/// 准备游戏地图,但稍后再显示它

fn setup_map_hidden(
    mut commands: Commands,
) {
    commands.spawn((
        GameMapEntity,
        SceneBundle {
            scene: todo!(),
            visibility: Visibility::Hidden,
            ..Default::default()
        },
    ));
}

/// When everything is ready, un-hide the game map
/// 当一切准备就绪,取消隐藏游戏地图
fn reveal_map(
    mut query: Query<&mut Visibility, With<GameMapEntity>>,
) {
    let mut vis_map = query.single_mut();
    *vis_map = Visibility::Visible;
}

可见性组件

在 Bevy 中,可见性由多个?组件表示:

任何代表游戏世界中可渲染对象的实体都需要拥有它们Bevy 的所有内置捆绑类型都包含它们。

如果您在不使用这些捆绑包的情况下创建自定义实体,则可以使用以下方法之一来确保您不会错过它们:

fn spawn_special_entity(
    mut commands: Commands,
) {
    // create an entity that does not use one of the common Bevy bundles,
    // 创建一个不使用普通Bevy bundle的实体,
    // but still needs transforms and visibility
    // 但仍然需要转换和可见性
    commands.spawn((
        ComponentA,
        ComponentB,
        SpatialBundle {
            transform: Transform::from_scale(Vec3::splat(3.0)),
            visibility: Visibility::Hidden,
            ..Default::default()
        },
    ));
}

如果您没有正确执行此操作(例如,您仅手动添加Visibility?组件并忘记其他组件,因为您不使用捆绑包),您的实体将不会呈现!

Visibility

Visibility是“面向用户的切换”。您可以在此处指定当前实体所需的内容:

  • Inherited(默认):根据父级显示/隐藏? ?,
  • Visible:始终显示实体,无论父实体如何
  • Hidden:始终隐藏实体,无论父实体如何

如果当前实体有任何具有 Inherited子实体,则当您将当前实体设置为Visible?或Hidden时,它们的可见性将受到影响。

如果一个实体有父实体,但父实体缺少与可见性相关的组件,则事物的行为就像没有父实体一样。

InheritedVisibility

InheritedVisibility表示当前实体基于其父实体的可见性而具有的状态。

InheritedVisibility的值应被视为只读。它由 Bevy 内部管理,其方式类似于变换传播。一个“可见性传播”系统在时间表?PostUpdate中运行。

如果您想读取当前帧的最新值,您应该将?您的系统添加计划?PostUpdate?中并在
?VisibilitySystems::VisibilityPropagate
之后对其进行排序。

/// Check if a specific UI button is visible
/// 检查特定的UI按钮是否可见
/// (could be hidden if the whole menu is hidden?)
/// (如果整个菜单都隐藏了,也可以隐藏吗?)
fn debug_player_visibility(
    query: Query<&InheritedVisibility, With<MyAcceptButton>>,
) {
    let vis = query.single();

    debug!("Button visibility: {:?}", vis.get());
}
 
    
use bevy::render::view::VisibilitySystems;

app.add_systems(PostUpdate,
    debug_player_visibility
        .after(VisibilitySystems::VisibilityPropagate)
);

ViewVisibility

ViewVisibility代表 Bevy 做出的关于是否需要渲染该实体的实际最终决定。

ViewVisibility?的值是只读的。它由 Bevy 进行内部管理。

它用于“剔除”:如果实体不在任何Camera或Light的范围内,则不需要渲染,因此Bevy将隐藏它以提高性能。

每一帧,在“可见性传播”之后,Bevy 都会检查哪些视图(相机或灯光)可以看到哪些实体,并将结果存储在这些组件中。

如果您想读取当前帧的最新值,您应该将?您的系统添加计划?PostUpdate中并在VisibilitySystems::CheckVisibility?之后对其进行排序。

/// Check if balloons are seen by any Camera, Light, etc… (not culled)
/// 检查气球是否被任何相机、灯光等看到…(未剔除)
fn debug_balloon_visibility(
    query: Query<&ViewVisibility, With<Balloon>>,
) {
    for vis in query.iter() {
        if vis.get() {
            debug!("Balloon will be rendered.");
        }
    }
}
 
    
use bevy::render::view::VisibilitySystems;

app.add_systems(PostUpdate,
    debug_balloon_visibility
        .after(VisibilitySystems::CheckVisibility)
);

Time and Timers - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

5.4 时间和计时器

相关官方示例:?timers、?move_sprite


时间

资源是您的主要全球计时信息来源,您可以从任何需要时间操作的系统访问该资源。您应该从中 得出所有时间Time您应该从中 得出所有时间

Bevy 在每一帧开始时更新这些值。

德尔塔时间

最常见的用例是“增量时间”——上一帧更新与当前帧更新之间经过了多少时间。这会告诉您游戏运行的速度,以便您可以缩放运动和动画等内容。这样,无论游戏的帧速率如何,一切都可以顺利发生并以相同的速度运行。

fn asteroids_fly(
    time: Res<Time>,
    mut q: Query<&mut Transform, With<Asteroid>>,
) {
    for mut transform in q.iter_mut() {
        // move our asteroids along the X axis
        // 沿着X轴移动我们的小行星
        // at a speed of 10.0 units per second
        // 以每秒10.0个单位的速度
        transform.translation.x += 10.0 * time.delta_seconds();
    }
}

持续时间

Time还可以为您提供自启动以来的总运行时间。如果您需要累积、增加、测量时间,请使用此功能。

use std::time::Instant;

/// Say, for whatever reason, we want to keep track
/// 不管出于什么原因,我们想要跟踪数据
/// of when exactly some specific entities were spawned.
/// 特定实体何时生成。
#[derive(Component)]
struct SpawnedTime(Instant);

fn spawn_my_stuff(
    mut commands: Commands,
    time: Res<Time>,
) {
    commands.spawn((/* ... */))
        // we can use startup time and elapsed duration
        // 我们可以使用启动时间和经过的持续时间
        .insert(SpawnedTime(time.startup() + time.elapsed()))
        // or just the time of last update
        // 或者只是上一次更新的时间
        .insert(SpawnedTime(time.last_update().unwrap()));
}

计时器和秒表

还有一些工具可以帮助您跟踪特定的间隔或时间:?TimerStopwatch。您可以创建许多此类实例,以跟踪您想要的任何内容。您可以在自己的组件资源类型中使用它们。

计时器和秒表需要勾选。您需要进行一些系统调用.tick(delta)才能使其开始运行,否则它将处于非活动状态。增量应该来自Time资源。

计时器

Timer允许您检测特定时间间隔何时过去。计时器有设定的持续时间。它们可以是“重复”或“不重复”。

两种类型都可以手动“重置”(从头开始计算时间间隔)和“暂停”(即使您继续勾选它们也不会进行)。

重复计时器在达到设定的持续时间后会自动重置。

.finished()用于检测计时器何时达到其设定的持续时间。如果您需要仅在达到持续时间时检测确切的刻度,请使用.just_finished()

use std::time::Duration;

#[derive(Component)]
struct FuseTime {
    /// track when the bomb should explode (non-repeating timer)
    /// 跟踪炸弹何时爆炸(非重复计时器)
    timer: Timer,
}

fn explode_bombs(
    mut commands: Commands,
    mut q: Query<(Entity, &mut FuseTime)>,
    time: Res<Time>,
) {
    for (entity, mut fuse_timer) in q.iter_mut() {
        // timers gotta be ticked, to work
        // 定时器必须被计时才能工作
        fuse_timer.timer.tick(time.delta());

        // if it finished, despawn the bomb
        //如果它完成了,销毁炸弹
        if fuse_timer.timer.finished() {
            commands.entity(entity).despawn();
        }
    }
}

#[derive(Resource)]
struct BombsSpawnConfig {
    /// How often to spawn a new bomb? (repeating timer)
    /// 多久生成一个新炸弹?(重复计时器)
    timer: Timer,
}

/// Spawn a new bomb in set intervals of time
/// 在设定的时间间隔内生成一个新的炸弹
fn spawn_bombs(
    mut commands: Commands,
    time: Res<Time>,
    mut config: ResMut<BombsSpawnConfig>,
) {
    // tick the timer
    // 计时
    config.timer.tick(time.delta());

    if config.timer.finished() {
        commands.spawn((
            FuseTime {
                // create the non-repeating fuse timer
                // 创建非重复的fuse定时器
                timer: Timer::new(Duration::from_secs(5), TimerMode::Once),
            },
            // ... other components ...
            // ……其他组件…
        ));
    }
}

/// Configure our bomb spawning algorithm
/// 配置炸弹生成算法
fn setup_bomb_spawning(
    mut commands: Commands,
) {
    commands.insert_resource(BombsSpawnConfig {
        // create the repeating timer
        // 创建重复计时器
        timer: Timer::new(Duration::from_secs(10), TimerMode::Repeating),
    })
}

请注意,Bevy 的计时器的工作方式与典型的现实生活中的计时器(向下计数到零)不同Bevy 的计时器从零开始计时,直至?设定的持续时间。它们基本上就像具有额外功能的秒表:最大持续时间和可选的自动重置。

跑表

Stopwatch允许您跟踪自某个点以来已经过去了多少时间。

它只会不断累积时间,您可以使用?.elapsed()/.elapsed_secs()检查。您可以随时手动重置它。

use bevy::time::Stopwatch;

#[derive(Component)]
struct JumpDuration {
    time: Stopwatch,
}

fn jump_duration(
    time: Res<Time>,
    mut q_player: Query<&mut JumpDuration, With<Player>>,
    kbd: Res<Input<KeyCode>>,
) {
    // assume we have exactly one player that jumps with Spacebar
    // 假设我们只有一个玩家使用空格键跳跃
    let mut jump = q_player.single_mut();

    if kbd.just_pressed(KeyCode::Space) {
        jump.time.reset();
    }

    if kbd.pressed(KeyCode::Space) {
        println!("Jumping for {} seconds.", jump.time.elapsed_secs());
        // stopwatch has to be ticked to progress
        // 秒表必须被计时才能继续
        jump.time.tick(time.delta());
    }
}

非官方 Bevy 作弊书

5.5 日志记录、控制台消息

相关官方例子:?logs.


您可能已经注意到,当您运行 Bevy 项目时,您会在控制台窗口中收到消息。例如:

2022-06-12T13:28:25.445644Z  WARN wgpu_hal::vulkan::instance: Unable to find layer: VK_LAYER_KHRONOS_validation
2022-06-12T13:28:25.565795Z  INFO bevy_render::renderer: AdapterInfo { name: "AMD Radeon RX 6600 XT", vendor: 4098, device: 29695, device_type: DiscreteGpu, backend: Vulkan }
2022-06-12T13:28:25.565795Z  INFO mygame: Entered new map area.

像这样的日志消息可以来自 Bevy、依赖项(如 wgpu),也可以来自您自己的代码。

Bevy 提供了一个日志框架,它比简单地使用 Rust 中的println/eprintln先进得多?。日志消息可以包含元数据,例如级别、时间戳和其来源的 Rust 模块。您可以看到此元数据与消息内容一起打印。

这是由 Bevy's 的LogPlugin设置的。它是?DefaultPlugins插件组的一部分,因此大多数 Bevy 用户将在每个典型的 Bevy 项目中自动拥有它。

级别

级别决定消息的重要性,并允许过滤消息。

可用级别有:offerrorwarninfodebugtrace

何时使用每个级别的粗略指南可能是:

  • off:禁用所有日志消息
  • error:发生了一些事情,导致事情无法正常工作
  • warn:发生了不寻常的事情,但事情可以继续进行
  • info:一般信息消息
  • debug:用于开发,有关代码正在执行的操作的消息
  • trace:非常详细的调试数据,例如转储值

打印您自己的日志消息

要显示消息,只需使用以消息级别命名的宏即可。语法与 Rust 的println语法完全相同。请参阅?std::fmt文档了解更多详细信息。

error!("Unknown condition!");
warn!("Something unusual happened!");
info!("Entered game level: {}", level_id);
debug!("x: {}, state: {:?}", x, state);
trace!("entity transform: {:?}", transform);

过滤消息

要控制您希望看到的消息,您可以配置 Bevy 的?LogPlugin

use bevy::log::LogPlugin;

app.add_plugins(DefaultPlugins.set(LogPlugin {
    filter: "info,wgpu_core=warn,wgpu_hal=warn,mygame=debug".into(),
    level: bevy::log::Level::DEBUG,
}));

filter字段是一个字符串,指定为不同 Rust 模块/板条箱启用哪个级别的规则列表。在上面的示例中,字符串的含义是:默认情况下显示到信息,将wgpu_core和wgpu_hal限制为警告级别,用于mygame show debug。

高于指定级别的所有级别也会启用。低于指定级别的所有级别均被禁用,并且不会显示这些消息。

level过滤器是对要使用的最低级别的全局限制。低于该级别的消息将被忽略,并且可以避免大部分性能开销。

环境变量

您可以在运行应用程序时使用环境变量RUST_LOG覆盖过滤器字符串?。

RUST_LOG="warn,mygame=debug" ./mygame

请注意,其他 Rust 项目(例如cargo)也使用相同的环境变量来控制其日志记录。这可能会导致意想不到的后果。例如:

RUST_LOG="debug" cargo run

将导致您的控制台也充满来自cargo 的调试消息。

调试和发布版本的不同设置

如果您想在 Rust 代码中为调试/发布构建执行不同的操作,实现它的一个简单方法是在“调试断言”上使用条件编译。

use bevy::log::LogPlugin;

// this code is compiled only if debug assertions are enabled (debug mode)
// 仅当启用调试断言时(调试模式),此代码才会编译
#[cfg(debug_assertions)]
app.add_plugins(DefaultPlugins.set(LogPlugin {
    level: bevy::log::Level::DEBUG,
    filter: "debug,wgpu_core=warn,wgpu_hal=warn,mygame=debug".into(),
}));

// this code is compiled only if debug assertions are disabled (release mode)
// 只有在禁用调试断言(release模式)时,此代码才会被编译
#[cfg(not(debug_assertions))]
app.add_plugins(DefaultPlugins.set(LogPlugin {
    level: bevy::log::Level::INFO,
    filter: "info,wgpu_core=warn,wgpu_hal=warn".into(),
}));

这是您不应仅出于性能原因在开发期间使用发布模式的一个很好的理由。

在 Microsoft Windows 上,您的游戏 EXE 还将启动一个控制台窗口,默认显示日志消息。您可能不希望在发布版本中出现这种情况。?看这里。

性能影响

将消息打印到控制台是一个相对较慢的操作。

但是,如果您不打印大量消息,请不要担心。只需避免从代码的性能敏感部分(例如内部循环)发送大量消息即可。

您可以在发布版本中禁用日志级别,例如tracedebug。


Parent/Child Hierarchies - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

5.6 分层(父/子)实体

相关官方示例:?hierarchy、?parenting


从技术上讲,实体/组件本身无法形成层次结构(ECS是平面数据结构)。然而,逻辑层次结构是游戏中的常见模式。

Bevy 支持在实体之间创建这样的逻辑链接,通过简单地在各个实体上添加Parent和?Children组件来形成虚拟“层次结构”。

当使用命令生成实体时,?Commands具有向实体添加子项的方法,该方法会自动添加正确的组件:

// spawn the parent and get its Entity id
// 生成父元素并获取其实体id
let parent = commands.spawn(MyParentBundle::default()).id();

// do the same for the child
// 对child做同样的操作
let child = commands.spawn(MyChildBundle::default()).id();

// add the child to the parent
// 将子元素添加到父元素
commands.entity(parent).push_children(&[child]);

// you can also use `with_children`:
// 你也可以使用`with_children`:
commands.spawn(MyParentBundle::default())
    .with_children(|parent| {
        parent.spawn(MyChildBundle::default());
    });

请注意,这仅设置Parent和?Children组件,而不设置其他任何内容。值得注意的是,它不会为您添加转换可见性。如果您需要该功能,您需要自己添加这些组件,使用类似SpatialBundle.

您可以使用单个命令来消除整个层次结构:

fn close_menu(
    mut commands: Commands,
    query: Query<Entity, With<MainMenuUI>>,
) {
    for entity in query.iter() {
        // despawn the entity and its children
        // 移除实体和它的子实体
        commands.entity(entity).despawn_recursive();
    }
}

访问父母或孩子

要创建一个适用于层次结构的系统,您通常需要两个查询

  • 包含您需要从子实体中获取的组件
  • 包含您需要从父实体获得的组件

两个查询之一应包含适当的组件,以获取与另一个查询一起使用的实体 ID:

  • 在子查询中的Parent查询,如果您想迭代实体并查找其父级,或者
  • 在父查询中Children查询,如果您想迭代实体并查找其子实体

例如,如果我们想得到有一个父元素的相机变换(Camera) ,以及它们的父元素的全局变换:

fn camera_with_parent(
    q_child: Query<(&Parent, &Transform), With<Camera>>,
    q_parent: Query<&GlobalTransform>,
) {
    for (parent, child_transform) in q_child.iter() {
        // `parent` contains the Entity ID we can use
        // `parent`包含我们可以使用的实体ID
        // to query components from the parent:
        // 从父组件中查询组件:
        let parent_global_transform = q_parent.get(parent.get());

        // do something with the components
    }
}

再举一个例子,假设我们正在制作一款策略游戏,并且我们有一些单位是小队的子项。假设我们需要制作一个适用于每个小队的系统,并且它需要一些有关孩子们的信息:

fn process_squad_damage(
    q_parent: Query<(&MySquadDamage, &Children)>,
    q_child: Query<&MyUnitHealth>,
) {
    // get the properties of each squad
?   // 获取每个小队的属性
    for (squad_dmg, children) in q_parent.iter() {
        // `children` is a collection of Entity IDs
?       // `children`是实体id的集合
        for &child in children.iter() {
            // get the health of each child unit
?           // 获取每个子单元的健康度
            let health = q_child.get(child);

            // do something
        }
    }
}

变换和可见性传播

如果您的实体代表“游戏世界中的对象”,您可能希望子级受到父级的影响。

变换传播允许子级相对于其父级定位并与其一起移动。

如果您手动隐藏其父级,则可见性传播允许隐藏子级。

Bevy 附带的大多数捆绑包都会自动提供这些行为。检查您正在使用的捆绑包的文档。例如,相机包具有变换,但不具有可见性。

否则,您可以使用它SpatialBundle来确保您的实体拥有所有必要的组件。

已知的陷阱

消失的子实体

如果你销毁一个有父实体的实体,Bevy 不会把它从父实体的子实体中移除。

如果您随后查询该父实体的子实体,您将得到一个无效实体,并且任何操作它的尝试都可能导致此错误:

thread 'main' panicked at 'Attempting to create an EntityCommands for entity 7v0, which doesn't exist.'

“尝试为实体7v0创建一个 EntityCommand,但实体7v0并不存在。”

解决方法是在 despawn 旁边手动调用 move _ children::

    commands.entity(parent_entity).remove_children(&[child_entity]);
    commands.entity(child_entity).despawn();

Fixed Timestep - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

5.7 固定时间步长

相关官方例子:?fixed_timestep.


如果您需要以固定的时间间隔发生某些事情(常见的用例是物理更新),您可以使用 Bevy 的FixedTimestep?Run Criteria将相应的系统添加到您的应用程序中。

use bevy::time::FixedTimestep;

// The timestep says how many times to run the SystemSet every second
// timestep表示每秒运行SystemSet的次数
// For TIMESTEP_1, it's once every second
// 对于TIMESTEP_1,它是每秒执行一次
// For TIMESTEP_2, it's twice every second
// 对于TIMESTEP_2,它是每秒两次

const TIMESTEP_1_PER_SECOND: f64 = 60.0 / 60.0;
const TIMESTEP_2_PER_SECOND: f64 = 30.0 / 60.0;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_system_set(
            SystemSet::new()
                // This prints out "hello world" once every second
?               // 这个函数每秒钟打印一次"hello world
                .with_run_criteria(FixedTimestep::step(TIMESTEP_1_PER_SECOND))
                .with_system(slow_timestep)
        )
        .add_system_set(
            SystemSet::new()
                // This prints out "goodbye world" twice every second
?               // 这个函数每秒钟打印两次"goodbye world
                .with_run_criteria(FixedTimestep::step(TIMESTEP_2_PER_SECOND))
                .with_system(fast_timestep)
        )
        .run();
}

fn slow_timestep() {
    println!("hello world");
}

fn fast_timestep() {
    println!("goodbye world");
}

状态

您可以通过访问资源?FixedTimesteps?您可以通过访问资源来检查固定时间步长跟踪器的当前状态。这可以让您知道距离下次触发还剩多少时间,或者已经超出了多少时间。您需要标记您的固定时间步长。

请参阅官方示例,它说明了这一点。

注意事项

Bevy 固定时间步长的主要问题在于它是使用Run Criteria实现的。它不能与其他运行条件(例如states )结合使用。这使得它对于大多数项目来说无法使用,这些项目需要依赖于状态来实现主菜单/加载屏幕等。考虑使用?iyes_loopless,它不存在这个问题。

另请注意,您的系统仍然与所有正常系统一起作为常规帧更新周期的一部分被调用。所以,时间并不准确。

FixedTimestep 运行条件只是检查自上次运行系统以来已经过去了多长时间,并决定是否在当前帧中运行它们,或者根据需要多次运行它们。

危险!丢失事件!

默认情况下,Bevy 的事件可靠!它们仅持续 2 帧,之后就会丢失。如果您的固定时间步长系统接收事件,请注意如果帧速率高于固定时间步长的 2 倍,您可能会错过一些事件。

解决这个问题的一种方法是使用带有手动清除功能的事件。这使您可以控制事件持续的时间,但如果您忘记清除它们,也可能会泄漏/浪费内存。

非官方 Bevy 作弊书

6 一般图形功能

本章介绍 Bevy 中与 2D 和 3D 游戏相关的一般图形相关功能。

Bevy 的渲染由相机驱动/通过相机配置。每个相机实体都会导致 Bevy 渲染您的游戏世界,如通过相机上的 各个组件配置的那样。您可以通过将相关组件添加到相机并进行配置来启用各种不同的工作流程以及可选效果。

非官方 Bevy 作弊书

6.1 相机

相机驱动 Bevy 中的所有渲染。他们负责配置绘制什么、如何绘制以及在哪里绘制。

您必须至少拥有一个相机实体,才能显示任何内容!如果您忘记生成相机,您将得到一个空的黑屏。

在最简单的情况下,您可以使用默认设置创建相机。只需使用Camera2dBundleCamera3dBundle生成一个实体。它将简单地绘制所有可见的可渲染实体

本页提供了 Bevy 中相机的总体概述。另请参阅2D 相机3D 相机的专用页面。

实用建议:始终为您的相机实体创建标记组件,以便您可以轻松查询您的相机!

#[derive(Component)]
struct MyGameCamera;

fn setup(mut commands: Commands) {
    commands.spawn((
        Camera3dBundle::default(),
        MyGameCamera,
    ));
}

相机变换

相机具有变换功能,可用于定位或旋转相机。这就是移动相机的方式。

例如,请参阅这些食谱页面:

如果您正在制作游戏,您应该实现自己的自定义相机控制,使其适合您的游戏类型和游戏玩法。

变焦相机

不要使用变换比例来“缩放”相机!它只是拉伸图像,而不是“缩放”。它还可能导致其他问题和不兼容。使用投影进行缩放。

对于正交投影,请更改比例。对于透视投影,更改 FOV。FOV 模仿镜头变焦的效果。

了解有关如何在2D或?3D中执行此操作的更多信息。

投影

相机投影负责将坐标系映射到视口(通常是屏幕/窗口)。它配置坐标空间以及图像的任何缩放/拉伸。

Bevy 提供两种投影:?OrthographicProjection和?PerspectiveProjection。它们是可配置的,能够服务于各种不同的用例。

正交意味着所有物体始终显示相同的尺寸,无论距相机有多远。

透视意味着物体距离相机越远,看起来就越小。这种效果赋予 3D 图形深度感和距离感。

2D 相机始终是正交的。

3D 相机可以使用任何一种投影。透视是最常见(也是默认)的选择。正交对于 CAD 和工程等应用程序非常有用,在这些应用程序中,您希望准确表示对象的尺寸,而不是创建逼真的 3D 空间感。一些游戏(尤其是模拟游戏)使用正交文字作为艺术选择。

可以实现您自己的自定义相机投影。这可以让您完全控制坐标系。但是,请注意,如果您违反 Bevy 的坐标系约定,事情可能会以意想不到的方式运行!

HDR 和色调映射

看这里!

渲染目标

相机的渲染目标决定了 GPU 将物体绘制到的位置。它可以是一个窗口(用于直接输出到屏幕)或??Image资产(渲染到纹理)。

默认情况下,摄像机输出到主窗口。

use bevy::render::camera::RenderTarget;

fn debug_render_targets(
    q: Query<&Camera>,
) {
    for camera in &q {
        match &camera.target {
            RenderTarget::Window(wid) => {
                eprintln!("Camera renders to window with id: {:?}", wid);
            }
            RenderTarget::Image(handle) => {
                eprintln!("Camera renders to image asset with id: {:?}", handle);
            }
            RenderTarget::TextureView(_) => {
                eprintln!("This is a special camera that outputs to something outside of Bevy.");
            }
        }
    }
}

视口

视口是一种将相机限制在其渲染目标的子区域(定义为矩形)的(可选)方法。该矩形实际上被视为要绘制的“窗口”。

一个明显的用例是分屏游戏,您希望相机仅绘制到屏幕的一半。

use bevy::render::camera::Viewport;

fn setup_minimap(mut commands: Commands) {
    commands.spawn((
        Camera2dBundle {
            camera: Camera {
                // renders after / on top of other cameras
                // 渲染后/在其他相机之上
                order: 2,
                // set the viewport to a 256x256 square in the top left corner
                // 将视口设置为左上角的256x256正方形
                viewport: Some(Viewport {
                    physical_position: UVec2::new(0, 0),
                    physical_size: UVec2::new(256, 256),
                    ..default()
                }),
                ..default()
            },
            ..default()
        },
        MyMinimapCamera,
    ));
}

如果您需要找出相机渲染到的区域(视口,如果配置,或整个窗口,如果没有):

fn debug_viewports(
    q: Query<&Camera, With<MyExtraCamera>>,
) {
    let camera = q.single();

    // the size of the area being rendered to
    // 渲染区域的大小
    let view_dimensions = camera.logical_viewport_size().unwrap();

    // the coordinates of the rectangle covered by the viewport
    // 视口覆盖的矩形的坐标
    let rect = camera.logical_viewport_rect().unwrap();
}

坐标转换

Camera提供了帮助屏幕坐标和世界空间坐标之间坐标转换的方法。有关示例,请参阅“光标到世界”食谱页面。

颜色清晰

这是在相机渲染任何内容之前整个视口将被清除的“背景颜色”。

如果您想保留所有像素之前的样子,您还可以禁用相机上的清除功能。

请参阅此页面了解更多信息。

渲染层

RenderLayers是一种过滤哪些实体应该由哪些相机绘制的方法。将此组件插入到您的实体上,将它们放置在特定的“层”中。层数是从 0 到 31 的整数(总共 32 个可用层)。

将此组件插入到相机实体上可以选择相机应渲染的图层。将此组件插入到可渲染实体上可以选择哪些相机应渲染这些实体。如果相机的图层和实体的图层之间存在任何重叠(它们至少有一个共同的图层),则将渲染实体。

如果实体没有该RenderLayers组件,则假定它(仅)属于第 0 层。

 
    
use bevy::render::view::visibility::RenderLayers;
// This camera renders everything in layers 0, 1
// 这个相机在0,1层渲染所有内容
commands.spawn((
    Camera2dBundle::default(),
    RenderLayers::from_layers(&[0, 1])
));
// This camera renders everything in layers 1, 2
// 这个相机在1、2层渲染所有内容
commands.spawn((
    Camera2dBundle::default(),
    RenderLayers::from_layers(&[1, 2])
));
// This sprite will only be seen by the first camera
// 这个精灵只会被第一个摄像头看到
commands.spawn((
    SpriteBundle::default(),
    RenderLayers::layer(0),
));
// This sprite will be seen by both cameras
// 这个精灵会被两个摄像头看到
commands.spawn((
    SpriteBundle::default(),
    RenderLayers::layer(1),
));
// This sprite will only be seen by the second camera
// 这个精灵只会被第二个摄像头看到
commands.spawn((
    SpriteBundle::default(),
    RenderLayers::layer(2),
));
// This sprite will also be seen by both cameras
// 这个精灵也会被两个摄像头看到
commands.spawn((
    SpriteBundle::default(),
    RenderLayers::from_layers(&[0, 2]),
));

您还可以在生成实体后修改它们的渲染层。

相机订购

相机order是一个简单的整数值,它控制相对于具有相同渲染目标的任何其他相机的顺序。

例如,如果您有多个摄像机全部渲染到主窗口,它们将表现为多个“层”。具有较高顺序值的相机将在具有较低值的相机“顶部”渲染。0是默认值。

use bevy::core_pipeline::clear_color::ClearColorConfig;

commands.spawn((
    Camera2dBundle {
        camera_2d: Camera2d {
            // no "background color", we need to see the main camera's output
            // 没有“背景色”,我们需要查看主摄像头的输出
            clear_color: ClearColorConfig::None,
            ..default()
        },
        camera: Camera {
            // renders after / on top of the main camera
            // 渲染后/在主摄像机顶部
            order: 1,
            ..default()
        },
        ..default()
    },
    MyOverlayCamera,
));

界面渲染

Bevy UI 渲染已集成到相机中!默认情况下,每个相机也会绘制 UI。

但是,如果您使用多个摄像头,您可能只希望 UI 绘制一次(可能由主摄像头绘制)。您可以禁用其他相机上的 UI 渲染。

此外,Bevy 中多个摄像头上的 UI 目前已损坏。即使您想要多个 UI 摄像头(例如,在具有多个窗口的应用程序中显示 UI),它也无法正常工作。

commands.spawn((
    Camera3dBundle::default(),
    // UI config is a separate component
    // UI配置是一个独立的组件
    UiCameraConfig {
        show_ui: false,
    },
    MyExtraCamera,
));

禁用相机

您可以停用相机而无需使其消失。当您想要保留相机实体及其携带的所有配置时,这非常有用,以便您以后可以轻松地重新启用它。

一些示例用例:切换覆盖、在 2D 和 3D 视图之间切换。

fn toggle_overlay(
    mut q: Query<&mut Camera, With<MyOverlayCamera>>,
) {
    let mut camera = q.single_mut();
    camera.is_active = !camera.is_active;
}

多个摄像头

这是您需要多个相机实体的不同场景的概述。

多个窗口

官方示例:multiple_windows.

如果要创建具有多个窗口的 Bevy 应用程序,则需要生成多个摄像机,每个窗口一个,并分别设置它们的渲染目标。然后,您可以使用相机来控制每个窗口中显示的内容。

分屏

官方示例:split_screen.

您可以将相机视口设置为仅渲染到渲染目标的一部分。这样,相机就可以渲染屏幕的一半(或任何其他区域)。在分屏游戏中为每个视图使用单独的摄像机。

叠加层

官方示例:two_passes.

您可能想要将多个“层”(通道)渲染到同一渲染目标。一个示例可能是显示在主游戏顶部的覆盖层/HUD。

覆盖相机可能与主相机完全不同。例如,主相机可能绘制 3D 场景,而叠加相机可能绘制 2D 形状。这样的用例是可能的!

使用单独的相机来创建叠加层。将优先级设置得?更高,以告诉 Bevy 在主摄像机之后(之上)渲染它。确保禁用清除

考虑一下您希望哪个摄像头负责渲染 UI。如果您希望覆盖层相机不受影响,请使用覆盖层相机;如果您希望覆盖层位于 UI 之上,请使用主相机。在另一台相机上禁用它。

使用渲染层来控制每个摄像机应渲染哪些实体。

渲染到图像

(又名渲染到纹理)

官方示例:render_to_texture.

如果要在内存中生成图像,可以输出到资产Image

这对于游戏中的中间步骤非常有用,例如在射击游戏中渲染小地图或枪。然后,您可以使用该图像作为最终场景的一部分渲染到屏幕上。项目预览是一个类似的用例。

另一个用例是想要生成图像文件的无窗口应用程序。例如,您可以使用 Bevy 渲染某些内容,然后将其导出到 PNG 文件。


HDR and Tonemapping - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

6.2 高动态范围

HDR(高动态范围)是指游戏引擎处理非常明亮的灯光或颜色的能力。Bevy 的渲染内部是 HDR。这意味着您可以拥有颜色高于1.0、非常明亮的灯光或明亮的发射材料的物体。所有这些都支持 3D 和 2D。

不要将其与 HDR 显示输出混淆,后者是生成 HDR 图像并由具有 HDR 功能的现代显示器或电视显示的能力。Bevy 尚未对此提供支持。

内部 HDR 图像必须先转换为 SDR(标准动态范围),然后才能显示在屏幕上。这个过程称为?色调映射。Bevy 支持不同的算法,可能会产生不同的外观。在游戏中使用哪种色调映射算法是一种艺术选择。

相机HDR配置

每个摄像机都有一个切换开关,可让您决定是否希望 Bevy 在内部保留 HDR 数据,以便后续通道(例如后处理效果)可以使用它。

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

如果启用,Bevy 的中间纹理将采用 HDR 格式。着色器输出 HDR 值,Bevy 将存储它们,以便它们可以在以后的渲染过程中使用。这允许您启用Bloom等利用 HDR 数据的效果。在不再需要 HDR 数据后,色调映射将作为后处理步骤进行。

如果禁用,着色器将输出 0.0 到 1.0 范围内的标准 RGB 颜色。色调映射发生在着色器中。不保留 HDR 信息。需要 HDR 数据的效果(例如 Bloom)将不起作用。

默认情况下它是禁用的,因为这可以为不需要它的简单图形应用程序带来更好的性能并减少 VRAM 使用量。

如果您同时启用了 HDR 和 MSAA,则可能会遇到问题。在某些情况下可能会出现视觉伪影。它在 Web/WASM 上也不支持,运行时会崩溃。如果您遇到任何此类问题,请禁用 MSAA。

色调映射

色调映射是渲染过程的步骤,其中像素的颜色从引擎内的中间表示转换为应在屏幕上显示的最终值。

这对于 HDR 应用程序非常重要,因为在这种情况下,图像可能包含非常亮的像素(高于 1.0),需要将其重新映射到可以显示的范围。

默认情况下启用色调映射。Bevy 允许您通过 ( Tonemapping) 组件针对每个摄像机进行配置。不建议禁用它,除非您知道只有非常简单的图形不需要它。它会使您的图形看起来不正确。

 
    
use bevy::core_pipeline::tonemapping::Tonemapping;

commands.spawn((
    Camera3dBundle {
        // no tonemapping
        // 没有色调映射
        tonemapping: Tonemapping::None,
        ..default()
    },
));
commands.spawn((
    Camera3dBundle {
        // this is the default:
        // 这是默认值:
        tonemapping: Tonemapping::TonyMcMapface,
        ..default()
    },
));
commands.spawn((
    Camera3dBundle {
        // another common choice:
        // 另一个常见的选择:
        tonemapping: Tonemapping::ReinhardLuminance,
        ..default()
    },
));

Bevy 支持许多不同的色调映射算法。它们中的每一个都会产生不同的外观,影响颜色和亮度。这可以是一种艺术选择。您可以决定哪种算法最适合您的游戏。Bevy 的默认值是 TonyMcMapface,尽管名字很傻,但它为各种图形样式提供了非常好的结果。???????有关每个可用选项的说明,请参阅 (Tonemapping ) 文档。

一些色调映射算法(包括默认的 TonyMcMapface)需要?tonemapping_luts?cargo feature。默认情况下它是启用的。如果您禁用默认功能并且需要它,请务必重新启用它。启用它还可以启用ktx2zstd功能,因为它的工作原理是将 KTX2 格式的特殊数据嵌入到游戏中,在色调映射过程中使用。

以下色调映射算法不需要来自?tonemapping_luts?的特殊数据:

  • Reinhard
  • ReinhardLuminance
  • AcesFitted
  • SomewhatBoringDisplayTransform

以下色调映射算法需要来自 ???????tonemapping_luts?的特殊数据:

  • AgX
  • TonyMcMapface
  • BlenderFilmic

如果您想制作较小的游戏二进制文件(可能对网页游戏很重要),您可以通过将默认色调映射更改为更简单的内容并禁用?cargo features.来减少膨胀。

颜色分级

颜色分级是对图像整体外观的处理。

与色调映射一起,这会影响最终图像的“色调”/“情绪”。

这也是实现“视网膜”效果的方法,其中相机通过调整曝光/伽玛动态适应非常黑暗(例如在洞穴内)和非常明亮(例如在日光下)的场景。

您还可以调整颜色饱和度。严重降低图像的饱和度可能会导致灰度或柔和的外观,这对于世界末日或恐怖游戏来说是一个很好的艺术选择。

您可以通过???????ColorGrading?组件配置这些参数:

use bevy::render::view::ColorGrading;

commands.spawn((
    Camera3dBundle {
        color_grading: ColorGrading {
            exposure: 0.0,
            gamma: 1.0,
            pre_saturation: 1.0,
            post_saturation: 1.0,
        },
        ..default()
    },
));

解带抖动

去带抖动有助于颜色渐变或其他颜色发生细微变化的区域显得更高质量,而不会出现“色带”效果。

它默认启用,并且可以针对每个摄像机禁用。

use bevy::core_pipeline::tonemapping::DebandDither;

commands.spawn((
    Camera3dBundle {
        dither: DebandDither::Disabled,
        ..default()
    },
));

以下是不带抖动(顶部)和带抖动(底部)的示例图像。注意地平面上绿色渐变的质量/平滑度。在具有逼真图形的游戏中,类似的情况可能会出现在天空、黑暗的房间或发出光晕效果的灯光中。

禁用/启用抖动的情况下,平坦绿色平面上场景简单立方体的视觉比较。????


Bloom - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

6.3 盛开?Bloom

“绽放”效果会在明亮的灯光周围产生辉光。尽管它的灵感来自光线透过肮脏或不完美的镜头的样子,但它并不是物理上精确的效果。

Bloom 在帮助感知非常亮的光线方面做得很好,尤其是在不支持将 HDR 输出到显示硬件时。您的显示器只能显示一定的最大亮度,因此布卢姆是一种常见的艺术选择,试图传达比可显示更亮的光强度。

Bloom 使用色调映射算法看起来效果最好,该算法可以降低非常明亮的颜色的饱和度。Bevy的默认是一个不错的选择。

Bloom 需要在您的相机上启用HDR 模式。将?BloomSettings组件添加到相机以启用光晕并配置效果。

use bevy::core_pipeline::bloom::BloomSettings;

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

绽放设置

Bevy 提供了许多参数来调整光晕效果的外观。

默认模式是“能量守恒”,这更接近真实光物理的表现。它试图模仿光散射的效果,而不人为地使图像变亮。效果更加微妙和“自然”。

还有一种“附加”模式,它会使所有东西变亮,让人感觉明亮的灯光不自然地“发光”。这种效果在许多游戏中都很常见,尤其是 2000 年代的老游戏。

Bevy 提供三种绽放“预设”:

  • NATURAL:节能、微妙、自然的外观。
  • OLD_SCHOOL:“发光”效果,类似于旧游戏的外观。
  • SCREEN_BLUR:非常强烈的绽放,使一切看起来都很模糊。

您还可以根据自己的喜好调整???????BloomSettings的所有参数来创建完全自定义的配置。使用预设来获取灵感。

以下是 Bevy 预设的设置:

 
    
// NATURAL
// 自然
BloomSettings {
    intensity: 0.15,
    low_frequency_boost: 0.7,
    low_frequency_boost_curvature: 0.95,
    high_pass_frequency: 1.0,
    prefilter_settings: BloomPrefilterSettings {
        threshold: 0.0,
        threshold_softness: 0.0,
    },
    composite_mode: BloomCompositeMode::EnergyConserving,
};

// OLD_SCHOOL
// 老学校
BloomSettings {
    intensity: 0.05,
    low_frequency_boost: 0.7,
    low_frequency_boost_curvature: 0.95,
    high_pass_frequency: 1.0,
    prefilter_settings: BloomPrefilterSettings {
        threshold: 0.6,
        threshold_softness: 0.2,
    },
    composite_mode: BloomCompositeMode::Additive,
};

// SCREEN_BLUR
// 屏幕模糊
BloomSettings {
    intensity: 1.0,
    low_frequency_boost: 0.0,
    low_frequency_boost_curvature: 0.0,
    high_pass_frequency: 1.0 / 3.0,
    prefilter_settings: BloomPrefilterSettings {
        threshold: 0.0,
        threshold_softness: 0.0,
    },
    composite_mode: BloomCompositeMode::EnergyConserving,
};

可视化

以下是 3D Bloom 的示例:

路灯的绽放效果。????

这是一个 2D 示例:

简单六边形上的绽放效果。????


(本节结束了,后边是7使用 2D

文章来源:https://blog.csdn.net/zhanglz888/article/details/135698502
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。