非官方 Bevy 作弊书07-09

源自 网页 Working with 2D - Unofficial Bevy Cheat Book

个人用 有道 翻译,希望能够帮助像我一样的 英语不好 的 bevy 初学者


非官方 Bevy 作弊书

7使用 bevy 2D

本章涵盖与使用 Bevy 制作 2D 游戏相关的主题。


2D Camera Setup - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

7.1 2D 相机设置

Bevy 中的相机必须看到任何东西:它们配置渲染。

本页将向您介绍 2D 相机的详细信息。如果您想了解一般的非 2D 特定功能,请参阅相机的一般页面

创建 2D 相机

Bevy 提供了一个bundle ( Camera2dBundle),您可以使用它来生成相机实体。它有合理的默认值来正确设置一切。

您可能需要设置变换来定位相机。

rust 复制代码
#[derive(Component)]
struct MyCameraMarker;

fn setup_camera(mut commands: Commands) {
    commands.spawn((
        Camera2dBundle {
            transform: Transform::from_xyz(100.0, 200.0, 0.0),
            ..default()
        },
        MyCameraMarker,
    ));
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup_camera)
        .run();
}

投影

投影决定了坐标如何映射到 视口(通常屏幕/窗口)。

2D 相机始终使用正交投影。

当您使用 Camera2dBundle 生成 2D 相机时,它会将组件 OrthographicProjection 添加到您的实体中。当您使用 2D 相机并且想要访问投影时,您应该 查询.OrthographicProjection

rust 复制代码
fn debug_projection(
    query_camera: Query<&OrthographicProjection, With<MyCameraMarker>>,
) {
    let projection = query_camera.single();
    // ... do something with the projection
}

请注意,这与3D不同。如果您正在制作一个库或其他一些应该能够处理 2D 和 3D 的代码,则无法进行单个查询来访问 2D 和 3D 相机。您应该创建单独的系统或至少两个单独的查询来处理每种类型的相机。这是有道理的,因为无论如何您可能需要不同的 2D 与 3D 逻辑。

注意:近/远值

投影包含nearfar值,它们指示相对于相机位置 ( transform ) 的最小和最大 Z 坐标(深度)。可渲染的transform ) 的最小和最大 Z 坐标(深度)。

Camera2dBundle将它们适当地设置为 2D: -1000.0 到 1000.0,允许实体显示在正 Z 坐标和负 Z 坐标上。但是,如果您自己创建 OrthographicProjection,则要更改任何其他设置,您需要自己设置这些值。该 OrthographicProjection结构的默认值是为 3D 设计的,值为near0.0这意味着您可能无法看到 2D 实体。

rust 复制代码
commands.spawn((
    Camera2dBundle {
        projection: OrthographicProjection {
            // don't forget to set `near` and `far`
            // 别忘了设置`near`和`far`
            near: -1000.0,
            far: 1000.0,
            // ... any other settings you want to change ...
            / /......您还想更改其他设置吗?
            ..default()
        },
        ..default()
    },
    MyCameraMarker,
));

一个更简单的方法是使用临时变量,让包做它的事情,然后改变你想要的任何东西。这样,您就不必担心确切的值或出现任何错误:

rust 复制代码
let mut camera_bundle = Camera2dBundle::default();
// change the settings we want to change:
// 更改我们想要更改的设置:
camera_bundle.projection.scale = 2.0;
camera_bundle.transform.rotate_z(30f32.to_radians());
// ...

commands.spawn((
    camera_bundle,
    MyCameraMarker,
));

缩放模式

您可以根据要处理窗口的大小/分辨率的方式来设置ScalingMode

Bevy 2D 相机的默认设置是 1 个屏幕像素对应 1 个世界单位,因此允许您以"像素"来思考一切。当调整窗口大小时,会导致看到更多或更少的内容。

如果您想保留此窗口调整大小行为,但更改屏幕像素到世界单位的映射,请使用ScalingMode::WindowSize(x),并设定一个不同于1.0的值。该值表示一个世界单位的屏幕像素数。

相反,如果您希望无论分辨率如何,始终在屏幕上显示相同数量的内容,则应该使用类似 ScalingMode::FixedVerticalScalingMode::AutoMax的内容。然后,您可以直接指定要在屏幕上显示多少个单位,并且您的内容将适当放大/缩小以适应窗口大小。

rust 复制代码
use bevy::render::camera::ScalingMode;

let mut my_2d_camera_bundle = Camera2dBundle::default();
// For this example, let's make the screen/window height correspond to
// 在这个例子中,让我们将screen/window的高度设置为
// 1600.0 world units. The width will depend on the aspect ratio.
// 1600.0世界单位宽度取决于宽高比。
my_2d_camera_bundle.projection.scaling_mode = ScalingMode::FixedVertical(1600.0);
my_2d_camera_bundle.transform = Transform::from_xyz(100.0, 200.0, 0.0);

commands.spawn((
    my_2d_camera_bundle,
    MyCameraMarker,
));

变焦

要在 2D 中"缩放",您可以更改正交投影的scale. 这使您可以通过某种因素来缩放所有内容,而不管 ScalingMode行为如何。

rust 复制代码
fn zoom_scale(
    mut query_camera: Query<&mut OrthographicProjection, With<MyCameraMarker>>,
) {
    let mut projection = query_camera.single_mut();
    // zoom in     // 放大
    projection.scale /= 1.25;
    // zoom out     // 缩小
    projection.scale *= 1.25;
}

或者,您可以重新配置ScalingMode. 这样您就可以对坐标/单位如何准确地映射到屏幕充满信心。这也有助于避免 2D 资源(尤其是像素艺术)出现缩放伪影。

rust 复制代码
fn zoom_scalingmode(
    mut query_camera: Query<&mut OrthographicProjection, With<MyCameraMarker>>,
) {
    use bevy::render::camera::ScalingMode;

    let mut projection = query_camera.single_mut();
    // 4 screen pixels to world/game pixel
    // 4个屏幕像素对应世界/游戏像素
    projection.scaling_mode = ScalingMode::WindowSize(4.0);
    // 6 screen pixels to world/game pixel
    // 6个屏幕像素对应世界/游戏像素
    projection.scaling_mode = ScalingMode::WindowSize(6.0);
}

考虑拥有一个预定义的"缩放级别"/比例值列表,以便您可以确保您的游戏始终看起来不错。

如果您正在制作像素艺术游戏,并且希望像素显得清晰而不是模糊,则需要确保将默认纹理过滤模式设置为"最近"(而不是"线性"):

rust 复制代码
fn main() {
    App::new()
        .add_plugins(
            DefaultPlugins
                .set(ImagePlugin::default_nearest())
        )
        // ...
        .run();
}

但是,在缩小尺寸时,首选线性(默认)过滤以获得更高的质量。因此,对于具有高分辨率资源的游戏,您希望保持其不变。


Sprites and Atlases - Unofficial Bevy Cheat Book
非官方 Bevy 作弊书

7.2 精灵和地图集

页面即将推出...

同时,你可以学习Bevy的例子


Working with 3D - Unofficial Bevy Cheat Book
非官方 Bevy 作弊书

8 使用 bevy 3D

本章涵盖与使用 Bevy 制作 3D 游戏相关的主题。


3D Camera Setup - Unofficial Bevy Cheat Book
非官方 Bevy 作弊书

8.1 3D 相机设置

Bevy 中的相机必须看到任何东西:它们配置渲染。

本页将向您介绍 3D 相机的详细信息。如果您想了解一般的非 3D 特定功能,请参阅相机的一般页面

创建 3D 相机

Bevy 提供了一个,您可以使用它来生成相机实体。它有合理的默认值来正确设置一切。

您可能需要设置变换来定位相机。

rust 复制代码
#[derive(Component)]
struct MyCameraMarker;

fn setup_camera(mut commands: Commands) {
    commands.spawn((
        Camera3dBundle {
            transform: Transform::from_xyz(10.0, 12.0, 16.0)
                .looking_at(Vec3::ZERO, Vec3::Y),
            ..default()
        },
        MyCameraMarker,
    ));
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup_camera)
        .run();
}

"查看"功能是确定 3D 相机方向的简单方法。第二个参数(我们提供为Y)是"向上"方向。如果你想让相机向侧面倾斜,你可以使用其他东西。如果你想制作一个自上而下的相机,直接向下看,你需要使用Y.

投影

投影决定了坐标如何映射到 视口(通常屏幕/窗口)。

3D 相机可以使用透视投影或正交投影。透视是默认且最常见的选择。

当您使用 Bevy 的捆绑包 ( Camera3dBundle) 生成 3D 相机时,它会将 组件 Projection添加到您的 实体中,这是一个enum,允许使用任一投影类型。

当您使用 3D 相机并想要访问投影时,您应该查询组件Projection类型。然后,您可以匹配枚举,以适当地处理每种情况。

rust 复制代码
fn debug_projection(
    query_camera: Query<&Projection, With<MyCameraMarker>>,
) {
    let projection = query_camera.single();
    match projection {
        Projection::Perspective(persp) => {
            // we have a perspective projection
            // 我们有了一个透视投影
        }
        Projection::Orthographic(ortho) => {
            // we have an orthographic projection
            // 我们有一个正投影
        }
    }
}

请注意,这与2D不同。如果您正在制作一个库或其他一些应该能够处理 2D 和 3D 的代码,则无法进行单个查询来访问 2D 和 3D 相机。您应该创建单独的系统或至少两个单独的查询来处理每种类型的相机。这是有道理的,因为无论如何您可能需要不同的 2D 与 3D 逻辑。

透视投影

透视创造了逼真的 3D 空间感。物体距离相机越远,看起来就越小。这就是人眼和现实生活中的相机所呈现的样子。

这里最重要的变量是 FOV(视野)。FOV 决定了透视效果的强度。FOV 是屏幕/图像高度所覆盖的角度。

更大的视场就像广角相机镜头。它让一切显得更加遥远、拉长、"缩小"。您可以在屏幕上看到更多内容。

较小的视场就像长焦相机镜头。它使一切看起来更近、更平坦,"放大"。您在屏幕上看到的内容更少。

作为参考,良好的中性值为 45°(较窄,Bevy 默认值)或 60°(较宽)。90°很宽。30°非常窄。

rust 复制代码
commands.spawn((
    Camera3dBundle {
        projection: PerspectiveProjection {
            // We must specify the FOV in radians.
            // 我们必须以弧度指定FOV
            // Rust can convert degrees to radians for us.
            // Rust可以将度数转换为弧度
            fov: 60.0_f32.to_radians(),
            ..default()
        }.into(),
        transform: Transform::from_xyz(10.0, 12.0, 16.0)
            .looking_at(Vec3::ZERO, Vec3::Y),
        ..default()
    },
    MyCameraMarker,
));

不同FOV值的并排比较。

在上图中,我们将视场减半/加倍,并将相机放置的距离加倍/减半,以进行补偿。请注意,您可以看到几乎相同的 3D 内容,但视场角越高,看起来就越拉伸,并且具有更强的 3D 透视效果。

在内部,Bevy 的透视投影使用无限反向 Z配置。这使得附近和远处的物体都具有良好的数值精度,避免了视觉伪影。

变焦

要"缩放",请更改透视投影的 FOV。

rust 复制代码
fn zoom_perspective(
    mut query_camera: Query<&mut Projection, With<MyCameraMarker>>,
) {
    // assume perspective. do nothing if orthographic.
    // 假设透视图如果正字法,什么都不做。
    let Projection::Perspective(persp) = query_camera.single_mut().into_inner() else {
        return;
    };
    // zoom in // 放大
    persp.fov /= 1.25;
    // zoom out  // 缩小
    persp.fov *= 1.25;
}

如果相机不移动,则减小 FOV 会使一切看起来更近,而增加 FOV 会使一切看起来更远:

"放大"(小FOV)和"缩出"(大FOV)3D场景的并排比

将此与移动相机本身(使用 变换)靠近或远离,同时保持 FOV 相同进行对比:

在某些应用程序(例如 3D 编辑器)中,移动相机可能比更改 FOV 更好。

正交投影

正交投影使所有物体看起来始终具有相同的尺寸,无论距相机的距离如何。感觉就像 3D 被压缩成 2D 一样。

正交对于 CAD 和工程等应用程序非常有用,您可以在这些应用程序中准确地表示对象的尺寸。某些游戏(尤其是模拟游戏)可能会使用正交文字作为艺术选择。

对于某些人来说,正交可能会感到困惑和不直观,因为它不会创建任何 3D 空间感。你无法判断任何事物有多远。它创造了完美的"扁平"外观。当从自上而下的对角线角度显示时,这种艺术风格有时被称为"等距"。

您应该根据您想要如何处理窗口大小/分辨率来设置ScalingMode

rust 复制代码
use bevy::render::camera::ScalingMode;

commands.spawn((
    Camera3dBundle {
        projection: OrthographicProjection {
            // For this example, let's make the screen/window height
            // 在这个例子中,让我们设定屏幕/窗口的高度
            // correspond to 16.0 world units.
            // 对应16.0个世界单位
            scaling_mode: ScalingMode::FixedVertical(16.0),
            ..default()
        }.into(),
        // the distance doesn't really matter for orthographic,
        // 对于正字法来说,距离并不重要,
        // it should look the same (though it might affect
        // 它看起来应该是一样的(尽管它可能会影响
        // shadows and clipping / culling)
        // 阴影和裁剪/剔除)
        transform: Transform::from_xyz(10.0, 12.0, 16.0)
            .looking_at(Vec3::ZERO, Vec3::Y),
        ..default()
    },
    MyCameraMarker,
));

带正交投影的3D场景可视化

变焦

要"缩放",请更改正交投影的比例。比例决定了场景的可见程度。

rust 复制代码
fn zoom_orthographic(
    mut query_camera: Query<&mut Projection, With<MyCameraMarker>>,
) {
    // assume orthographic. do nothing if perspective.
    // 假设正字法。什么都不做,如果透视。
    let Projection::Orthographic(ortho) = query_camera.single_mut().into_inner() else {
        return;
    };
    // zoom in  // 放大
    ortho.scale /= 1.25;
    // zoom out  // 缩小
    ortho.scale *= 1.25;
}

3D中不同正交投影比例的并排比较


3D Models and Scenes (GLTF) - Unofficial Bevy Cheat Book
非官方 Bevy 作弊书

8.2 3D 模型和场景 (GLTF)

相关官方示例: load_gltfupdate_gltf_scene


Bevy 对 3D 资源使用 GLTF 2.0 文件格式。

(其他格式可能通过第 3 方插件非官方提供)

快速入门:将 3D 模型生成到您的世界中

最简单的用例是仅加载"3D 模型"并将其生成到游戏世界中。

"3D 模型"通常很复杂,由多个部分组成。想象一栋房子:窗户、屋顶、门等都是独立的部分,可能由多种网格、材料和纹理制成。从技术上讲,Bevy 需要多个 ECS 实体来表示和渲染整个事物。

这就是为什么您的 GLTF"模型"由 Bevy 表示为 [Scene][cb::scene]。这样,您可以轻松地生成它,Bevy 将创建所有相关的子实体并正确配置它们。

rust 复制代码
fn spawn_gltf(
    mut commands: Commands,
    ass: Res<AssetServer>,
) {
    // note that we have to include the `Scene0` label
    // 注意,我们必须包含`Scene0`标签
    let my_gltf = ass.load("my.glb#Scene0");

    // to position our 3d model, simply use the Transform
    // 要定位我们的3d模型,只需使用Transform
    // in the SceneBundle
    // 在SceneBundle中
    commands.spawn(SceneBundle {
        scene: my_gltf,
        transform: Transform::from_xyz(2.0, 0.0, -5.0),
        ..Default::default()
    });
}

您还可以使用 GLTF 文件来加载整个地图/关卡。它的工作原理是一样的。

上面的示例假设您有一个简单的 GLTF 文件,仅包含一个"默认场景"。GLTF 是一种非常灵活的文件格式。单个文件可以包含许多"模型"或更复杂的"场景"。要更好地了解 GLTF 和可能的工作流程,请阅读本页的其余部分。:)

GLTF简介

GLTF 是一种现代开放标准,用于在不同 3D 软件应用程序(例如游戏引擎和 3D 建模软件)之间交换 3D 资产。

GLTF 文件格式有两种变体:人类可读的 ascii/文本 ( *.gltf) 和二进制 ( *.glb)。二进制格式更加紧凑,更适合将资源打包到游戏中。文本格式可能对开发有用,因为使用文本编辑器可以更轻松地手动检查。

GLTF 文件可以包含许多对象(子资源):网格、材质、纹理、场景、动画剪辑。加载 GLTF 文件时,Bevy 将加载其中包含的所有资源。它们将被映射到适当的 Bevy 内部资产类型

GLTF 子资产

GLTF 术语可能会令人困惑,因为与 Bevy 相比,它有时使用相同的词来指代不同的事物。本节将尝试解释各种 GLTF 术语。

为了理解一切,在头脑中考虑这些概念如何在不同的地方表示是有帮助的:在 3D 建模软件(如 Blender)中、在 GLTF 文件本身中以及在 Bevy 中。

GLTF场景是您在游戏世界中生成的内容。这通常是您在 3D 建模软件屏幕上看到的内容。场景结合了游戏引擎所需的所有数据,以创建所有需要的实体来表示您想要的内容。从概念上讲,将场景视为一个"单元"。根据您的用例,这可能是一个"3D 模型",甚至是整个地图或游戏关卡。在 Bevy 中,这些被表示为包含所有子 ECS 实体的 Bevy 场景。

GLTF 场景由 GLTF节点 组成。这些描述了场景中的"对象",通常是 GLTF 网格,但也可以是其他事物,例如相机和灯光。每个 GLTF 节点都有一个用于将其定位在场景中的变换。GLTF 节点没有核心 Bevy 等效项;Bevy 只是使用这些数据在场景内创建 ECS 实体。如果您需要访问此数据,Bevy 有一种特殊的 GltfNode 资产类型。

GLTF网格 代表一种概念性的"3D 对象"。它们对应于 3D 建模软件中的"对象"。GLTF 网格可能很复杂,由多个较小的部分组成,称为 GLTF 基元,每个部分可能使用不同的材质。GLTF 网格没有核心 Bevy 等效项,但有一种特殊的GltfMesh资源类型,用于描述基元。

GLTF基元 是用于渲染目的的单独"3D 几何单元"。它们包含实际的几何体/顶点数据,并引用绘图时要使用的材质。在 Bevy 中,每个 GLTF 原语都表示为 BevyMesh资产,并且必须生成为要渲染的单独 ECS 实体。

GLTF材质 描述 3D 模型表面的着色参数。他们完全支持基于物理的渲染(PBR)。他们还引用要使用的纹理。在 Bevy 中,它们表示为StandardMaterial资产,由 Bevy PBR 3D 渲染器使用。

GLTF纹理 (图像)可以嵌入 GLTF 文件内,也可以与它一起外部存储在单独的图像文件中。例如,您可以将纹理作为单独的 PNG/JPEG/KTX2 文件以便于开发,或者将它们全部打包在 GLTF 文件中以便于分发。在 Bevy 中,GLTF 纹理作为 Bevy Image 资源加载。

GLTF采样器 描述了 GPU 应如何使用给定纹理的设置。Bevy 并没有将它们分开;该数据存储在 Bevy Image资产( SamplerDescriptor 类型 的sampler字段)内。

GLTF动画 描述随时间插入各种值的动画,例如变换或网格骨架。在 Bevy 中,这些作为AnimationClip 资产加载。

GLTF 使用模式

单个 GLTF 文件可以包含任意数量的上述任何类型的子资源,并以任意方式相互引用。

由于 GLTF 非常灵活,因此如何构建资产取决于您。

可以使用单个 GLTF 文件:

  • 表示单个"3D 模型",包含带有模型的单个 GLTF 场景,以便您可以将其生成到游戏中。
  • 表示整个关卡,作为 GLTF 场景,可能还包括相机。这可以让您一次加载并生成整个关卡/地图。
  • 将关卡/地图的各个部分(例如房间)表示为单独的 GLTF 场景。如果需要,他们可以共享网格和纹理。
  • 包含一组许多不同的"3D 模型",每个模型作为一个单独的 GLTF 场景。这使您可以立即加载和管理整个集合,并根据需要单独生成它们。
  • ... 其他的?

用于创建 GLTF 资产的工具

如果您使用最新版本的 Blender (2.8+) 进行 3D 建模,则开箱即用地支持 GLTF。只需导出并选择 GLTF 作为格式即可。

对于其他工具,您可以尝试这些导出器插件:

请务必检查您的导出设置,以确保 GLTF 文件包含您期望的所有内容。

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

纹理

对于纹理/图像数据,GLTF 格式规范正式将支持的格式限制为 PNG、JPEG 或 Basis。然而,Bevy 并没有强制执行此类"人为限制"。您可以使用Bevy 支持的任何图像格式

您的 3D 编辑器可能会导出带有 PNG 纹理的 GLTF。这将"正常工作"并且非常适合简单的用例。

然而,mipmap 和压缩纹理对于获得良好的 GPU 性能、内存 (VRAM) 使用率和视觉质量非常重要。仅当您使用支持这些功能的 KTX2 或 DDS 等格式时,您才能获得这些好处。

我们建议您使用 KTX2,它本身支持所有 GPU 纹理功能 + 额外的zstd压缩,以减小文件大小。如果您这样做,请不要忘记启用Bevy 的ktx2zstd 货物功能。 cargo features货物功能。

您可以使用该klafsa工具将 GLTF 文件中使用的所有纹理从 PNG/JPEG 转换为 KTX2,并使用您选择的 mipmap 和 GPU 纹理压缩。

复制代码
TODO: show an example workflow for converting textures into the "optimal" format

TODO:展示一个将纹理转换为"最佳"格式的工作流示例

在 Bevy 中使用 GLTF 子资产

GLTF 文件中包含的各种子资产可以通过两种方式进行寻址:

  • 按索引(整数 ID,按照它们在文件中出现的顺序)
  • 按名称(文本字符串,您在创建资源时在 3D 建模软件中设置的名称,可以导出到 GLTF)

要获取 Bevy 中各个资产的句柄,您可以使用 Gltf "主资产",或者使用 带有标签的 AssetPath

Gltf主资产

如果您有一个复杂的 GLTF 文件,这可能是导航其内容和使用内部不同内容的最灵活和最有用的方法。

您必须等待 GLTF 文件加载,然后才能使用该Gltf资源。

rust 复制代码
use bevy::gltf::Gltf;

/// Helper resource for tracking our asset
/// 用于跟踪资源的辅助资源
#[derive(Resource)]
struct MyAssetPack(Handle<Gltf>);

fn load_gltf(
    mut commands: Commands,
    ass: Res<AssetServer>,
) {
    let gltf = ass.load("my_asset_pack.glb");
    commands.insert_resource(MyAssetPack(gltf));
}

fn spawn_gltf_objects(
    mut commands: Commands,
    my: Res<MyAssetPack>,
    assets_gltf: Res<Assets<Gltf>>,
) {
    // if the GLTF has loaded, we can navigate its contents
    // 如果GLTF已经加载,我们可以导航它的内容
    if let Some(gltf) = assets_gltf.get(&my.0) {
        // spawn the first scene in the file
        // 生成文件中的第一个场景
        commands.spawn(SceneBundle {
            scene: gltf.scenes[0].clone(),
            ..Default::default()
        });

        // spawn the scene named "YellowCar"
        // 生成名为YellowCar的场景
        commands.spawn(SceneBundle {
            scene: gltf.named_scenes["YellowCar"].clone(),
            transform: Transform::from_xyz(1.0, 2.0, 3.0),
            ..Default::default()
        });

        // PERF: the `.clone()`s are just for asset handles, don't worry :)
        // PERF:不用担心,`.clone()`只是用来处理资源:)
    }
}

对于一个更复杂的示例,假设我们出于某种原因想要直接创建 3D PBR 实体。(不推荐这样做;您可能应该只使用场景)

rust 复制代码
use bevy::gltf::GltfMesh;

fn gltf_manual_entity(
    mut commands: Commands,
    my: Res<MyAssetPack>,
    assets_gltf: Res<Assets<Gltf>>,
    assets_gltfmesh: Res<Assets<GltfMesh>>,
) {
    if let Some(gltf) = assets_gltf.get(&my.0) {
        // Get the GLTF Mesh named "CarWheel"
        // 获取名为CarWheel的GLTF网格
        // (unwrap safety: we know the GLTF has loaded already)
        // (解包安全性:我们知道GLTF已经加载)
        let carwheel = assets_gltfmesh.get(&gltf.named_meshes["CarWheel"]).unwrap();

        // Spawn a PBR entity with the mesh and material of the first GLTF Primitive
        // 用第一个GLTF基元的网格和材质生成一个PBR实体
        commands.spawn(PbrBundle {
            mesh: carwheel.primitives[0].mesh.clone(),
            // (unwrap: material is optional, we assume this primitive has one)
            // (unwrap: material是可选的,我们假设这个基元有一个)
            material: carwheel.primitives[0].material.clone().unwrap(),
            ..Default::default()
        });
    }
}

带标签的 AssetPath

这是访问特定子资产的另一种方式。它不太可靠,但在某些情况下可能更容易使用。

使用AssetServer将路径字符串转换为 Handle.

优点是您可以立即获取子资产的句柄,即使您的 GLTF 文件尚未加载。

缺点是比较容易出错。如果您指定文件中实际不存在的子资源,或者错误输入标签,或者使用错误的标签,它就会默默地不起作用。此外,目前仅支持使用数字索引。您不能按名称来寻址子资产。

rust 复制代码
fn use_gltf_things(
    mut commands: Commands,
    ass: Res<AssetServer>,
) {
    // spawn the first scene in the file
    // 生成文件中的第一个场景
    let scene0 = ass.load("my_asset_pack.glb#Scene0");
    commands.spawn(SceneBundle {
        scene: scene0,
        ..Default::default()
    });

    // spawn the second scene
    // 生成第二个场景
    let scene1 = ass.load("my_asset_pack.glb#Scene1");
    commands.spawn(SceneBundle {
        scene: scene1,
        transform: Transform::from_xyz(1.0, 2.0, 3.0),
        ..Default::default()
    });
}

支持以下资产标签({}是数字索引):

  • Scene{}: GLTF 场景 作为 BevyScene
  • Node{}: GLTF 节点作为GltfNode
  • Mesh{}:GLTF 网格作为GltfMesh
  • Mesh{}/Primitive{}: GLTF 原始 作为 BevyMesh
  • Mesh{}/Primitive{}/MorphTargets:GLTF 基元的变形目标动画数据
  • Texture{}:GLTF 纹理作为 Bevy Image
  • Material{}: GLTF 材料 作为 BevyStandardMaterial
  • DefaultMaterial:如上所述,如果 GLTF 文件包含没有索引的默认材质
  • Animation{}: GLTF 动画 作为 BevyAnimationClip
  • Skin{}:GLTF 网格皮肤作为 BevySkinnedMeshInverseBindposes

GltfNodeGltfMesh 和资产类型仅有助于帮助您浏览 GLTF 文件的内容。它们不是核心 Bevy 渲染器类型,Bevy 不会以任何其他方式使用它们。Bevy 渲染器期望实体具有 MaterialMeshBundle; 为此,您需要 MeshStandardMaterial

bevy的限制

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

常见的限制:

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

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


Input Handling - Unofficial Bevy Cheat Book
非官方 Bevy 作弊书

9 输入处理

Bevy 支持以下输入:

支持以下著名的输入设备:

  • 用于设备倾斜的加速计和陀螺仪
  • 其他传感器,例如温度传感器
  • 在多点触控板上跟踪各个手指,就像在触摸屏上一样
  • 麦克风和其他音频输入设备
  • MIDI(乐器),但有一个非官方插件:bevy_midi.

对于大多数输入类型(只要有意义),Bevy 提供了两种处理它们的方法:

某些输入仅作为事件提供。

检查状态是使用诸如 Input(对于按键或按钮等二进制输入)、 Axis(对于模拟输入)、 Touches(对于触摸屏上的手指)等资源来完成的。这种处理输入的方式对于实现游戏逻辑非常方便。在这些场景中,您通常只关心映射到游戏中操作的特定输入。您可以检查特定按钮/按键以查看它们何时被按下/释放,或者它们的当前状态是什么。

事件输入事件)是一种较低级别、更全面的方法。如果您想从该类输入设备获取所有活动,而不是仅检查特定输入,请使用它们。

输入映射

Bevy 尚未提供内置方法来进行输入映射(配置键绑定等)。您需要想出自己的方式将输入转换为游戏/应用程序中的逻辑操作。

有一些社区制作的插件可能会对此有所帮助:请参阅 bevy-assets 上的输入部分。我个人推荐: Leafwing Studios 的输入管理器插件

构建您自己的特定于您的游戏的抽象可能是个好主意。例如,如果您需要处理玩家移动,您可能需要一个系统来读取输入并将其转换为您自己的内部"移动意图/动作事件",然后另一个系统作用于这些内部事件,以实际移动玩家玩家。确保使用明确的系统排序以避免滞后/帧延迟。

运行条件

如果您希望特定系统仅在按下特定键或按钮时运行,Bevy 还提供了可以附加到系统的运行条件请在此处查看所有条件)。

这样,您可以将输入处理作为 系统调度/配置的一部分,并避免在 CPU 上运行不必要的代码。

不建议在真实游戏中使用这些,因为您必须对按键进行硬编码,这使得无法进行用户可配置的按键绑定。

为了支持可配置的键绑定,您可以实现自己的运行条件,从用户设置中检查您的键绑定。

如果您使用LWIM 插件,它还提供对 类似的基于运行条件的工作流程的支持。


Keyboard - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

9.1 键盘输入

相关官方示例: keyboard_inputkeyboard_input_events


本页显示如何处理按下和释放的键盘按键。

如果您对文本输入感兴趣,请参阅字符输入页面。

注意:Mac 上的 Command 键对应于 PC 上的 Super/Windows 键。

检查按键状态

最常见的是,您可能对特定的已知键以及检测它们何时被按下或释放感兴趣。您可以使用Input<KeyCode> /资源检查特定的Input<ScanCode>资源检查特定的按键代码或扫描代码

rust 复制代码
fn keyboard_input(
    keys: Res<Input<KeyCode>>,
) {
    if keys.just_pressed(KeyCode::Space) {
        // Space was pressed
        // 空格被按下
    }
    if keys.just_released(KeyCode::LControl) {
        // Left Ctrl was released
        // 左侧Ctrl被释放
    }
    if keys.pressed(KeyCode::W) {
        // W is being held down
        // W被按住
    }
    // we can check multiple at once with `.any_*`
    if keys.any_pressed([KeyCode::LShift, KeyCode::RShift]) {
        // Either the left or right shift are being held down
        //要么是左移,要么是右移
    }
    if keys.any_just_pressed([KeyCode::Delete, KeyCode::Back]) {
        // Either delete or backspace was just pressed
        // 只是按下了delete或backspace
    }
}

键盘事件

要获取所有键盘活动,您可以使用 eventsKeyboardInput

rust 复制代码
fn keyboard_events(
    mut key_evr: EventReader<KeyboardInput>,
) {
    use bevy::input::ButtonState;

    for ev in key_evr.iter() {
        match ev.state {
            ButtonState::Pressed => {
                println!("Key press: {:?} ({})", ev.key_code, ev.scan_code);
            }
            ButtonState::Released => {
                println!("Key release: {:?} ({})", ev.key_code, ev.scan_code);
            }
        }
    }
}

这些事件为您提供Key 代码和扫描代码。

键码和扫描码

键盘按键可以通过按键码或扫描码来识别。

键码代表每个键的逻辑含义(通常是符号/字母,或其执行的功能)。它们取决于用户操作系统中当前活动的键盘布局。Bevy 用枚举来表示它们KeyCode

扫描代码代表键盘上的物理键,无论系统布局如何。Bevy 使用 表示它们ScanCode,其中包含一个整数 ID。整数的确切值是没有意义的并且取决于操作系统,但是键盘上的给定物理键将始终产生相同的值,无论用户的语言和键盘布局设置如何。

键绑定的最佳实践

以下是一些关于如何为您的游戏实现用户友好的可重映射键绑定的建议,这些建议非常适合国际用户或使用非 QWERTY 键盘布局的用户。

本节假设您已经实现了某种系统来允许用户重新配置其键绑定。您希望提示用户按他们喜欢的键来执行给定的游戏内操作,以便您可以存储/记住它并在以后玩游戏时使用它。

问题是,如果您只是使用按键代码,那么用户可能会在游戏中意外切换操作系统键盘布局,并突然让键盘无法按预期工作。

您应该使用扫描代码检测并存储用户选择的按键,并在游戏过程中使用扫描代码检测键盘输入。

键代码仍可用于 UI 目的,例如向用户显示所选键。


Mouse - Unofficial Bevy Cheat Book
非官方 Bevy 作弊书

9.2 鼠标

相关官方示例: mouse_inputmouse_input_events


鼠标按钮

与键盘输入类似,鼠标按钮可用作 Input状态资源事件运行条件请参阅列表)。使用最适合您的用例的模式。

您可以使用 Input<MouseButton>命令检查特定鼠标按钮的状态:

rust 复制代码
fn mouse_button_input(
    buttons: Res<Input<MouseButton>>,
) {
    if buttons.just_pressed(MouseButton::Left) {
        // Left button was pressed
        // 左键被按下
    }
    if buttons.just_released(MouseButton::Left) {
        // Left Button was released
        // 左键被释放
    }
    if buttons.pressed(MouseButton::Right) {
        // Right Button is being held down
        // 右键被持续按下
    }
    // we can check multiple at once with `.any_*`
    // 我们可以一次检查多个 `.any_*`
    if buttons.any_just_pressed([MouseButton::Left, MouseButton::Right]) {
        // Either the left or the right button was just pressed
        // 左键或右键刚被按下
    }
}

您还可以遍历已按下或释放的任何按钮:

rust 复制代码
fn mouse_button_iter(
    buttons: Res<Input<MouseButton>>,
) {
    for button in buttons.get_pressed() {
        println!("{:?} is currently held down", button);
    }
    for button in buttons.get_just_pressed() {
        println!("{:?} was pressed", button);
    }
    for button in buttons.get_just_released() {
        println!("{:?} was released", button);
    }
}

或者,您可以使用MouseButtonInput事件来获取所有活动:

rust 复制代码
use bevy::input::mouse::MouseButtonInput;

fn mouse_button_events(
    mut mousebtn_evr: EventReader<MouseButtonInput>,
) {
    use bevy::input::ButtonState;

    for ev in mousebtn_evr.iter() {
        match ev.state {
            ButtonState::Pressed => {
                println!("Mouse button press: {:?}", ev.button);
            }
            ButtonState::Released => {
                println!("Mouse button release: {:?}", ev.button);
            }
        }
    }
}

您还可以使用 Bevy 的内置运行条件,这样您的 系统仅在鼠标按钮输入时运行。仅推荐用于原型设计;对于适当的项目,您可能需要实现自己的运行条件,以支持重新绑定或其他自定义用例。

rust 复制代码
use bevy::input::common_conditions::*;

app.add_systems(Update, (
    handle_middleclick
        .run_if(input_just_pressed(MouseButton::Middle)),
    handle_drag
        .run_if(input_pressed(MouseButton::Left)),
));

鼠标滚动/滚轮

要检测滚动输入,请使用eventsMouseWheel

rust 复制代码
use bevy::input::mouse::MouseWheel;

fn scroll_events(
    mut scroll_evr: EventReader<MouseWheel>,
) {
    use bevy::input::mouse::MouseScrollUnit;
    for ev in scroll_evr.iter() {
        match ev.unit {
            MouseScrollUnit::Line => {
                println!("Scroll (line units): vertical: {}, horizontal: {}", ev.y, ev.x);
                //  println!("滚轮滚动(线单位):垂直:{}, 水平:{}", ev.y, ev.x);
            }
            MouseScrollUnit::Pixel => {
                println!("Scroll (pixel units): vertical: {}, horizontal: {}", ev.y, ev.x);
                //  println!("滚轮滚动(像素单位):垂直:{}, 水平:{}", ev.y, ev.x);
            }
        }
    }
}

枚举MouseScrollUnit很重要:它告诉您滚动输入的类型。Line适用于具有固定步长的硬件,例如桌面鼠标上的滚轮。Pixel适用于具有平滑(细粒度)滚动的硬件,例如笔记本电脑触摸板。

您可能应该以不同的方式处理这些问题(使用不同的灵敏度设置),以便在两种类型的硬件上提供良好的体验。

注意: 不保证单位Line具有整数值/步长!至少macOS在操作系统级别进行非线性缩放/滚动加速,这意味着即使使用带有固定步进滚轮的常规 PC 鼠标,您的应用程序也会获得奇怪的行数值。

鼠标运动

如果您不关心鼠标光标的确切位置,而只想查看它在帧之间移动了多少,请使用此选项。这对于控制 3D 相机等操作非常有用。

使用MouseMotion 事件。每当鼠标移动时,您都会收到一个带有增量的事件。

rust 复制代码
use bevy::input::mouse::MouseMotion;

fn mouse_motion(
    mut motion_evr: EventReader<MouseMotion>,
) {
    for ev in motion_evr.iter() {
        println!("Mouse moved: X: {} px, Y: {} px", ev.delta.x, ev.delta.y);
        //  println!("鼠标移动:X: {} px, Y: {} px", ev.delta.x, ev.delta.y);
    }
}

您可能想在游戏窗口内抓住/锁定鼠标

鼠标光标位置

如果您想准确跟踪指针/光标位置,请使用此选项。这对于单击游戏或 UI 中的对象并将其悬停在上面等操作非常有用。

您可以从相应的 Window获取鼠标指针的当前坐标(如果鼠标当前位于该窗口内):

rust 复制代码
use bevy::window::PrimaryWindow;

fn cursor_position(
    q_windows: Query<&Window, With<PrimaryWindow>>,
) {
    // Games typically only have one window (the primary window)
    // 游戏通常只有一个窗口(主窗口)
    if let Some(position) = q_windows.single().cursor_position() {
        println!("Cursor is inside the primary window, at {:?}", position);
        //  println!("光标位于主窗口内,在{:?}", position);
    } else {
        println!("Cursor is not in the game window.");
        // println!("光标不在游戏窗口内。");
    }
}

要检测指针何时移动,请使用事件 CursorMoved来获取更新的坐标:

rust 复制代码
fn cursor_events(
    mut cursor_evr: EventReader<CursorMoved>,
) {
    for ev in cursor_evr.iter() {
        println!(
            "New cursor position: X: {}, Y: {}, in Window ID: {:?}",
            //  "新的光标位置:X: {}, Y: {}, 在窗口ID: {:?}",
            ev.position.x, ev.position.y, ev.window
        );
    }
}

请注意,您只能获取鼠标在窗口内的位置;您无法获取鼠标在整个操作系统桌面/整个屏幕上的全局位置。

您获得的坐标位于"窗口空间"中。它们代表窗口像素,原点是窗口的左下角。它们与您的相机或游戏中的坐标没有任何关系。请参阅此食谱示例,了解将这些窗口光标坐标转换为世界空间坐标。

要跟踪鼠标光标何时进入和离开窗口,请使用 CursorEnteredCursorLeft events

触摸板手势

Bevy 支持两指旋转和捏合缩放手势,但它们目前仅适用于 macOS,操作系统为它们提供了特殊事件。

如果您有兴趣在应用程序中支持这些手势,您可以使用TouchpadRotate和TouchpadMagnify events来实现:

rust 复制代码
use bevy::input::touchpad::{TouchpadMagnify, TouchpadRotate};

// these only work on macOS
// 这些只在macOS上工作
fn touchpad_gestures(
    mut evr_touchpad_magnify: EventReader<TouchpadMagnify>,
    mut evr_touchpad_rotate: EventReader<TouchpadRotate>,
) {
    for ev_magnify in evr_touchpad_magnify.iter() {
        // Positive numbers are zooming in   // 正数表示放大
        // Negative numbers are zooming out  // 负数表示缩小
        println!("Touchpad zoom by {}", ev_magnify.0);
    }
    for ev_rotate in evr_touchpad_rotate.iter() {
        // Positive numbers are anticlockwise  // 正数表示逆时针
        // Negative numbers are clockwise      // 负数表示顺时针
        println!("Touchpad rotate by {}", ev_rotate.0);
    }
}

Text / Character - Unofficial Bevy Cheat Book
非官方 Bevy 作弊书

9.3 文本/字符输入

相关官方示例: char_input_eventstext_input


如果您想在 Bevy 应用程序中实现文本输入,请使用此功能(而不是 键盘输入)。这样,一切都会按照用户期望的操作系统运行,包括 Unicode 支持。

Bevy 将为来自操作系统的每个 Unicode 代码点生成一个ReceivedCharacter 事件。

此示例演示如何让用户将文本输入到字符串中(此处存储为本地资源)。

rust 复制代码
fn text_input(
    mut evr_char: EventReader<ReceivedCharacter>,
    kbd: Res<Input<KeyCode>>,
    mut string: Local<String>,
) {
    if kbd.just_pressed(KeyCode::Return) {
        println!("Text input: {}", &*string);
        string.clear();
    }
    if kbd.just_pressed(KeyCode::Back) {
        string.pop();
    }
    for ev in evr_char.iter() {
        // ignore control (special) characters
        // 忽略控制(特殊)字符
        if !ev.char.is_control() {
            string.push(ev.char);
        }
    }
}

注意:我们使用 Bevy 的常规键盘输入来处理 Enter 和 Backspace 键的按下。当按下这些键时也会发送字符事件(它们会产生特殊的控制字符,如 ASCII 换行符\n),因此,如果我们不希望将它们保存到字符串中,则需要忽略它们。

在您自己的应用程序中,您可能还希望以适合您的 UI 的方式处理箭头键等操作。

输入法支持

Bevy 支持 IME(输入法编辑器),这是人们在具有更复杂文字的语言(如东亚语言)中执行文本输入的方式。但是,它需要您进行一些特殊处理。

IME 通过使用特殊的"缓冲区"来工作,它显示当前正在进行的文本建议,并允许用户在确认之前选择正确的字符。文本建议/自动完成由操作系统提供,但您的应用程序需要向用户显示它们。

如果您希望所有国际用户都能够用他们的语言输入文本(就像他们在操作系统上的其他 GUI 应用程序中通常所做的那样),您应该支持 IME。

为此,每当您希望用户输入文本时,您都需要在窗口上启用"IME 模式",然后再将其禁用。例如,如果您在玩游戏之前提示用户输入姓名,则您可以在提示处于活动状态时启用 IME 模式。

启用"IME 模式"后,如果用户使用 IME,您将收到 Ime事件,而不是ReceivedCharacter 常规键盘输入。但是,如果用户没有使用 IME,那么即使启用了"IME 模式",一切也会正常运行。

当用户有正在进行的文本时,您将收到Ime::Preedit事件,告诉您"临时缓冲区"的当前内容以及需要显示的有关光标/突出显示的信息,以便用户可以看到他们正在做什么。

当用户确认他们的输入时,您将收到一个Ime::Commit事件,告诉您用户希望插入到应用程序中的文本。

rust 复制代码
// for this simple example, we will just enable/disable IME mode on mouse click
// 对于这个简单的例子,我们只需要在鼠标点击时启用/禁用IME模式
fn ime_toggle(
    mousebtn: Res<Input<MouseButton>>,
    mut q_window: Query<&mut Window, With<PrimaryWindow>>,
) {
    if mousebtn.just_pressed(MouseButton::Left) {
        let mut window = q_window.single_mut();

        // toggle "IME mode"
        // 切换"IME模式"
        window.ime_enabled = !window.ime_enabled;

        // We need to tell the OS the on-screen coordinates where the text will
        // 我们需要告诉OS文本在屏幕上的坐标
        // be displayed; for this simple example, let's just use the mouse cursor.
        // 显示;在这个简单的例子中,我们只使用鼠标光标。
        // In a real app, this might be the position of a UI text field, etc.
        // 在实际应用中,这可能是UI文本框的位置等
        window.ime_position = window.cursor_position().unwrap();
    }
}

fn ime_input(
    mut evr_ime: EventReader<Ime>,
) {
    for ev in evr_ime.iter() {
        match ev {
            Ime::Commit { value, .. } => {
                println!("IME confirmed text: {}", value);
                // println!("IME确认文本: {}", value);
            }
            Ime::Preedit { value, cursor, .. } => {
                println!("IME buffer: {:?}, cursor: {:?}", value, cursor);
                // println!("IME缓冲区: {:?}, 光标: {:?}", value, cursor);
            }
            Ime::Enabled { .. } => {
                println!("IME mode enabled!");
                // println!("输入法模式启用!");
            }
            Ime::Disabled { .. } => {
                println!("IME mode disabled!");
                // println!("输入法模式禁用!");
            }
        }
    }
}

为了简洁起见,此示例仅将事件打印到控制台。

在真实的应用程序中,您需要在屏幕上显示"预编辑"文本,并使用不同的格式来显示光标。在"提交"时,您可以将提供的文本附加到您通常接受文本输入的实际字符串中。


Gamepad (Controller, Joystick) - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

9.4 游戏手柄(控制器、操纵杆)

相关官方示例: gamepad_inputgamepad_input_events


Bevy 支持游戏手柄输入硬件:控制台控制器、操纵杆等。许多不同类型的硬件都应该可以工作,但如果您的设备不受支持,您应该向 gilrs项目提交问题。

游戏手柄 ID

Bevy为每个连接的游戏手柄分配一个唯一的 ID ( Gamepad)。这使您可以将设备与特定玩家关联起来,并区分您的输入来自哪一个。

您可以使用该Gamepads 资源列出当前连接的所有游戏手柄设备的 ID,或检查特定设备的状态。

要检测游戏手柄何时连接或断开连接,您可以使用 GamepadEvent events

显示如何记住第一个连接的游戏手柄 ID 的示例:

rust 复制代码
/// Simple resource to store the ID of the connected gamepad.
/// 存储连接手柄ID的简单资源。
/// We need to know which gamepad to use for player input.
/// 我们需要知道使用哪个手柄进行玩家输入。
#[derive(Resource)]
struct MyGamepad(Gamepad);

fn gamepad_connections(
    mut commands: Commands,
    my_gamepad: Option<Res<MyGamepad>>,
    mut gamepad_evr: EventReader<GamepadEvent>,
) {
    for ev in gamepad_evr.iter() {
        // the ID of the gamepad
        // 手柄的ID
        let id = ev.gamepad;
        match &ev.event_type {
            GamepadEventType::Connected(info) => {
                println!("New gamepad connected with ID: {:?}, name: {}", id, info.name);

                // if we don't have any gamepad yet, use this one
                // 如果我们还没有任何手柄,请使用这个
                if my_gamepad.is_none() {
                    commands.insert_resource(MyGamepad(id));
                }
            }
            GamepadEventType::Disconnected => {
                println!("Lost gamepad connection with ID: {:?}", id);

                // if it's the one we previously associated with the player,
                // 如果它是我们之前与玩家关联的那个元素,
                // disassociate it:
                // 解除关联:
                if let Some(MyGamepad(old_id)) = my_gamepad.as_deref() {
                    if *old_id == id {
                        commands.remove_resource::<MyGamepad>();
                    }
                }
            }
            // other events are irrelevant
            // 其他事件不相关
            _ => {}
        }
    }
}

处理游戏手柄输入

您可以使用Axis<GamepadAxis>(Axis, GamepadAxis).来处理模拟摇杆和触发器。按钮可以用 Input<GamepadButton> (Input, GamepadButton),类似于鼠标按钮键盘按键

请注意, GamepadButton 中按钮的名称与供应商无关(例如SouthEast,而不是 X/O 或 A/B)。

rust 复制代码
fn gamepad_input(
    axes: Res<Axis<GamepadAxis>>,
    buttons: Res<Input<GamepadButton>>,
    my_gamepad: Option<Res<MyGamepad>>,
) {
    let gamepad = if let Some(gp) = my_gamepad {
        // a gamepad is connected, we have the id
        // 手柄已连接,我们有id
        gp.0
    } else {
        // no gamepad is connected
        // 没有连接手柄
        return;
    };

    // The joysticks are represented using a separate axis for X and Y
    // 操纵杆使用单独的X轴和Y轴表示
    let axis_lx = GamepadAxis {
        gamepad, axis_type: GamepadAxisType::LeftStickX
    };
    let axis_ly = GamepadAxis {
        gamepad, axis_type: GamepadAxisType::LeftStickY
    };

    if let (Some(x), Some(y)) = (axes.get(axis_lx), axes.get(axis_ly)) {
        // combine X and Y into one vector
        // 将X和Y合并为一个向量
        let left_stick_pos = Vec2::new(x, y);

        // Example: check if the stick is pushed up
        // 例子:检查操纵杆是否向上推
        if left_stick_pos.length() > 0.9 && left_stick_pos.y > 0.5 {
            // do something
        }
    }

    // In a real game, the buttons would be configurable, but here we hardcode them
    // 在真实的游戏中,按钮是可配置的,但在这里我们对它们进行了硬编码
    let jump_button = GamepadButton {
        gamepad, button_type: GamepadButtonType::South
    };
    let heal_button = GamepadButton {
        gamepad, button_type: GamepadButtonType::East
    };

    if buttons.just_pressed(jump_button) {
        // button just pressed: make the player jump
        // 刚刚按下的按钮:让玩家跳跃
    }

    if buttons.pressed(heal_button) {
        // button being held down: heal the player
        // 按下按钮:治愈玩家
    }
}

您还可以使用 GamepadEvent 事件处理游戏手柄输入:

rust 复制代码
fn gamepad_input_events(
    my_gamepad: Option<Res<MyGamepad>>,
    mut gamepad_evr: EventReader<GamepadEvent>,
) {
    let gamepad = if let Some(gp) = my_gamepad {
        // a gamepad is connected, we have the id
        // 手柄已连接,我们有id
        gp.0
    } else {
        // no gamepad is connected
        // 没有连接手柄
        return;
    };

    for ev in gamepad_evr.iter() {
        if ev.gamepad != gamepad {
            // event not from our gamepad
            // 事件不是来自我们的手柄
            continue;
        }

        use GamepadEventType::{AxisChanged, ButtonChanged};

        match ev.event_type {
            AxisChanged(GamepadAxisType::RightStickX, x) => {
                // Right Stick moved (X)
                // 右摇杆移动(X)
            }
            AxisChanged(GamepadAxisType::RightStickY, y) => {
                // Right Stick moved (Y)
                // 右摇杆被移动(Y)
            }
            ButtonChanged(GamepadButtonType::DPadDown, val) => {
                // buttons are also reported as analog, so use a threshold
                // 按钮也是模拟按钮,所以请使用阈值
                if val > 0.5 {
                    // button pressed
                    // 按下按钮
                }
            }
            _ => {} // don't care about other inputs
                    // 不用关心其他输入
        }
    }
}

游戏手柄设置

您可以使用 GamepadSettings资源 来配置各个轴和按钮的死区和其他参数。您可以设置全局默认值,也可以单独设置每个轴/按钮。

下面是一个示例,展示了如何使用自定义设置来配置游戏手柄(不一定是好的设置,请不要盲目复制这些):

rust 复制代码
// this should be run once, when the game is starting
// 这应该在游戏开始时运行一次
// (transition entering your in-game state might be a good place to put it)
// (过渡状态可能是放置它的好地方)
fn configure_gamepads(
    my_gamepad: Option<Res<MyGamepad>>,
    mut settings: ResMut<GamepadSettings>,
) {
    let gamepad = if let Some(gp) = my_gamepad {
        // a gamepad is connected, we have the id
        // 手柄已连接,我们有id
        gp.0
    } else {
        // no gamepad is connected
        // 没有连接手柄
        return;
    };

    // add a larger default dead-zone to all axes (ignore small inputs, round to zero)
    // 为所有轴添加一个更大的默认死区(忽略小的输入,四舍五入为零)
    settings.default_axis_settings.set_deadzone_lowerbound(-0.1);
    settings.default_axis_settings.set_deadzone_upperbound(0.1);

    // make the right stick "binary", squash higher values to 1.0 and lower values to 0.0
    // 将右边的值设置为二进制,将较大的值压缩为1.0,较小的值压缩为0.0
    let mut right_stick_settings = AxisSettings::default();
    right_stick_settings.set_deadzone_lowerbound(-0.5);
    right_stick_settings.set_deadzone_upperbound(0.5);
    right_stick_settings.set_livezone_lowerbound(-0.5);
    right_stick_settings.set_livezone_upperbound(0.5);
    // the raw value should change by at least this much,
    // 原始值至少应该改变这么多,
    // for Bevy to register an input event:
    // 让Bevy注册输入事件:
    right_stick_settings.set_threshold(0.01);

    // make the triggers work in big/coarse steps, to get fewer events
    // 让触发器按大/粗的步骤工作,以获得更少的事件
    // reduces noise and precision
    // 降低噪声和精度
    let mut trigger_settings = AxisSettings::default();
    trigger_settings.set_threshold(0.25);

    // set these settings for the gamepad we use for our player
    // 为我们用于玩家的手柄设置这些设置
    settings.axis_settings.insert(
        GamepadAxis { gamepad, axis_type: GamepadAxisType::RightStickX },
        right_stick_settings.clone()
    );
    settings.axis_settings.insert(
        GamepadAxis { gamepad, axis_type: GamepadAxisType::RightStickY },
        right_stick_settings.clone()
    );
    settings.axis_settings.insert(
        GamepadAxis { gamepad, axis_type: GamepadAxisType::LeftZ },
        trigger_settings.clone()
    );
    settings.axis_settings.insert(
        GamepadAxis { gamepad, axis_type: GamepadAxisType::RightZ },
        trigger_settings.clone()
    );

    // for buttons (or axes treated as buttons):
    // 对于按钮(或者作为按钮的轴):
    let mut button_settings = ButtonSettings::default();
    // require them to be pressed almost all the way, to count
    // 几乎在整个过程中都需要按下它们来计数
    button_settings.set_press_threshold(0.9);
    // require them to be released almost all the way, to count
    // 几乎所有的过程中都需要释放它们
    button_settings.set_release_threshold(0.1);

    settings.default_button_settings = button_settings;
}

Touchscreen - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

9.5 触摸屏

相关官方示例: touch_inputtouch_input_events


支持多点触控触摸屏。您可以在屏幕上跟踪多个手指,并提供位置和压力/力信息。Bevy 不提供手势识别功能。

Touches允许您跟踪当前屏幕上的任何手指:

rust 复制代码
fn touches(
    touches: Res<Touches>,
) {
    // There is a lot more information available, see the API docs.
    // 还有更多可用的信息,请参阅 API 文档
    // This example only shows some very basic things.
    // 此示例只显示一些非常基本的内容。

    for finger in touches.iter() {
        if touches.just_pressed(finger.id()) {
            println!("A new touch with ID {} just began.", finger.id());
        }
        println!(
            "Finger {} is at position ({},{}), started from ({},{}).",
            finger.id(),
            finger.position().x,
            finger.position().y,
            finger.start_position().x,
            finger.start_position().y,
        );
    }
}

或者,您可以使用事件TouchInput

rust 复制代码
fn touch_events(
    mut touch_evr: EventReader<TouchInput>,
) {
    use bevy::input::touch::TouchPhase;
    for ev in touch_evr.iter() {
        // in real apps you probably want to store and track touch ids somewhere
        // 在真正的应用程序中,您可能希望在某个地方存储和跟踪 touch id
        match ev.phase {
            TouchPhase::Started => {
                println!("Touch {} started at: {:?}", ev.id, ev.position);
            }
            TouchPhase::Moved => {
                println!("Touch {} moved to: {:?}", ev.id, ev.position);
            }
            TouchPhase::Ended => {
                println!("Touch {} ended at: {:?}", ev.id, ev.position);
            }
            TouchPhase::Cancelled => {
                println!("Touch {} cancelled at: {:?}", ev.id, ev.position);
            }
        }
    }
}

复制代码
Drag-and-Drop (Files) - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

9.6 拖放(文件)

相关官方例子: drag_and_drop.


Bevy 支持大多数桌面操作系统上常见的拖放手势,但仅适用于文件,不适用于任意数据/对象。

如果您将文件(例如,从文件管理器应用程序)拖动到 Bevy 应用程序中,Bevy 将生成一个FileDragAndDrop ,其中包含放入的文件的路径。

rust 复制代码
fn file_drop(
    mut dnd_evr: EventReader<FileDragAndDrop>,
) {
    for ev in dnd_evr.iter() {
        println!("{:?}", ev);
        if let FileDragAndDrop::DroppedFile { id, path_buf } = ev {
            println!("Dropped file with path: {:?}, in window id: {:?}", path_buf, id);
        }
    }
}

检测拖放位置

您可能想要根据放下手势结束时光标所在的位置执行不同的操作。例如,如果将文件拖放到特定的 UI 元素/面板上,则将文件添加到某个集合。

不幸的是,由于winit bug #1550 ,目前实现起来有些棘手。当拖动手势正在进行时, Bevy 不会获取 CursorMoved events ,因此不会响应鼠标光标。bevy完全失去了光标位置的踪迹。

Window 检查光标位置也不起作用。

在拖动手势期间,使用光标事件来响应光标移动的系统将无法工作。这包括 Bevy UI 的Interaction 检测,这是检测 UI 元素何时悬停的常用方法。

解决方法

解决此问题的唯一方法是在收到放置事件后,将文件路径临时存储在某处。然后,等待下一个 CursorMoved事件,然后处理该文件。

请注意,这甚至可能不会出现在下一帧更新中。每当用户移动光标时,就会发生下一次光标更新。如果用户在放置文件后没有立即移动鼠标并将光标留在同一位置一段时间,则不会有任何事件,您的应用程序将无法知道光标位置。


(本节结束,请看下一节 10 窗口管理)