非官方 Bevy 作弊书02-03

Bevy Cookbook - Unofficial Bevy Cheat Book

源于上述网页,个人用 有道 进行翻译,希望对大家有所帮助

2 bevy食谱

本章向您展示如何使用 Bevy 做各种实际的事情。

每个页面都专注于一个特定问题,并提供解释和示例代码来教您如何解决它。

假设您已经熟悉 Bevy 编程

您还应该查看 Bevy 的官方示例集。引擎的几乎每个区域都有一些东西,尽管它们通常只显示 API 的简单用法,而没有太多解释。

如果您想要从头到尾遵循的分步教程,这些教程位于Bevy 教程章节中。


Show Framerate - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

2.1 显示帧率

您可以使用 Bevy 的内置诊断来测量帧速率 (FPS),以监控性能。

要启用它,请将 Bevy 的诊断插件添加到您的应用程序中:

use bevy::diagnostic::FrameTimeDiagnosticsPlugin;
app.add_plugins(FrameTimeDiagnosticsPlugin::default());

最简单的使用方法是将诊断信息打印到控制台 ( log )。如果您只想在开发版本中执行此操作,则可以添加条件编译属性。

#[cfg(debug_assertions)] // debug/dev builds only // 仅调试/dev生成
{
    use bevy::diagnostic::LogDiagnosticsPlugin;
    app.add_plugins(LogDiagnosticsPlugin::default());
}

游戏内/屏幕上的 FPS 计数器

您可以使用 Bevy UI 创建游戏内 FPS 计数器。

建议您创建一个具有绝对定位的新 UI 根(没有父级的实体),以便您可以控制 FPS 计数器出现的确切位置,并且不会影响 UI 的其余部分。

下面是一些示例代码,向您展示如何制作一个非常漂亮且可读的 FPS 计数器:

Code Example (Long):
use bevy::diagnostic::DiagnosticsStore;
use bevy::diagnostic::FrameTimeDiagnosticsPlugin;

/// Marker to find the container entity so we can show/hide the FPS counter
/// 标记以查找容器实体,以便我们可以显示/隐藏FPS计数器
#[derive(Component)]
struct FpsRoot;

/// Marker to find the text entity so we can update it
/// 标记以查找文本实体,以便我们可以更新它
#[derive(Component)]
struct FpsText;

fn setup_fps_counter(
    mut commands: Commands,
) {
    // create our UI root node   // 创建我们的UI根节点
    // this is the wrapper/container for the text   // 这是文本的包装器/容器
    let root = commands.spawn((
        FpsRoot,
        NodeBundle {
            // give it a dark background for readability
            // 为其提供深色背景以便于阅读
            background_color: BackgroundColor(Color::BLACK.with_a(0.5)),
            // make it "always on top" by setting the Z index to maximum
            // 通过将Z索引设置为最大值,使其“始终位于顶部”
            // we want it to be displayed over all other UI
            // 我们希望它显示在所有其他UI上
            z_index: ZIndex::Global(i32::MAX),
            style: Style {
                position_type: PositionType::Absolute,
                // position it at the top-right corner  // 将其放置在右上角
                // 1% away from the top window edge     // 距离顶部窗口边缘1%
                right: Val::Percent(1.),
                top: Val::Percent(1.),
                // set bottom/left to Auto, so it can be   
                // 将bottom/left设置为Auto,因此可以
                // automatically sized depending on the text   
                // 根据文本自动调整大小
                bottom: Val::Auto,
                left: Val::Auto,
                // give it some padding for readability
                // 给它一些可读性的填充
                padding: UiRect::all(Val::Px(4.0)),
                ..Default::default()
            },
            ..Default::default()
        },
    )).id();
    // create our text   // 创建我们的文本
    let text_fps = commands.spawn((
        FpsText,
        TextBundle {
            // use two sections, so it is easy to update just the number
            // 使用两个部分,所以只更新数字很容易
            text: Text::from_sections([
                TextSection {
                    value: "FPS: ".into(),
                    style: TextStyle {
                        font_size: 16.0,
                        color: Color::WHITE,
                        // if you want to use your game's font asset,
                        // 如果您想使用游戏的字体资产,
                        // uncomment this and provide the handle:
                        // 取消注释并提供句柄:
                        // font: my_font_handle
                        // font:my_font_handle
                        ..default()
                    }
                },
                TextSection {
                    value: " N/A".into(),
                    style: TextStyle {
                        font_size: 16.0,
                        color: Color::WHITE,
                        // if you want to use your game's font asset,
                        // 如果您想使用游戏的字体资产,
                        // uncomment this and provide the handle:
                        // 取消注释并提供句柄:
                        // font: my_font_handle
                        // font:my_font_handle
                        ..default()
                    }
                },
            ]),
            ..Default::default()
        },
    )).id();
    commands.entity(root).push_children(&[text_fps]);
}

fn fps_text_update_system(
    diagnostics: Res<DiagnosticsStore>,
    mut query: Query<&mut Text, With<FpsText>>,
) {
    for mut text in &mut query {
        // try to get a "smoothed" FPS value from Bevy
        // 尝试从Bevy获得“平滑”的FPS值
        if let Some(value) = diagnostics
            .get(FrameTimeDiagnosticsPlugin::FPS)
            .and_then(|fps| fps.smoothed())
        {
            // Format the number as to leave space for 4 digits, just in case,
            // 将数字格式化为留出4位数字的空间,以防万一,
            // right-aligned and rounded. This helps readability when the
            // 右对齐和圆角。当
            // number changes rapidly.
            // 数字变化很快。
            text.sections[1].value = format!("{value:>4.0}");

            // Let's make it extra fancy by changing the color of the
            // 让我们通过改变的颜色来让它变得更加花哨
            // text according to the FPS value:
            // 根据FPS值的文本:
            text.sections[1].style.color = if value >= 120.0 {
                // Above 120 FPS, use green color
                // 高于120 FPS,使用绿色
                Color::rgb(0.0, 1.0, 0.0)
            } else if value >= 60.0 {
                // Between 60-120 FPS, gradually transition from yellow to green
                // 在60-120 FPS之间,从黄色逐渐过渡到绿色
                Color::rgb(
                    (1.0 - (value - 60.0) / (120.0 - 60.0)) as f32,
                    1.0,
                    0.0,
                )
            } else if value >= 30.0 {
                // Between 30-60 FPS, gradually transition from red to yellow
                // 在30-60 FPS之间,从红色逐渐过渡到黄色
                Color::rgb(
                    1.0,
                    ((value - 30.0) / (60.0 - 30.0)) as f32,
                    0.0,
                )
            } else {
                // Below 30 FPS, use red color
                // 低于30 FPS,使用红色
                Color::rgb(1.0, 0.0, 0.0)
            }
        } else {
            // display "N/A" if we can't get a FPS measurement
            // 如果无法获得FPS测量值,则显示“N/A”
            // add an extra space to preserve alignment
            // 添加额外空间以保持对齐
            text.sections[1].value = " N/A".into();
            text.sections[1].style.color = Color::WHITE;
        }
    }
}

/// Toggle the FPS counter when pressing F12
/// 按F12时切换FPS计数器
fn fps_counter_showhide(
    mut q: Query<&mut Visibility, With<FpsRoot>>,
    kbd: Res<Input<KeyCode>>,
) {
    if kbd.just_pressed(KeyCode::F12) {
        let mut vis = q.single_mut();
        *vis = match *vis {
            Visibility::Hidden => Visibility::Visible,
            _ => Visibility::Hidden,
        };
    }
}
app.add_systems(Startup, setup_fps_counter);
app.add_systems(Update, (
    fps_text_update_system,
    fps_counter_showhide,
));

Convert cursor to world coordinates - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

2.2 将光标转换为世界坐标

2D游戏

如果您只有一个窗口(主窗口),就像大多数应用程序和游戏的情况一样,您可以执行以下操作:

Code (simple version):
use bevy::window::PrimaryWindow;

/// We will store the world position of the mouse cursor here.
/// 我们将在此处存储鼠标光标的世界位置。
#[derive(Resource, Default)]
struct MyWorldCoords(Vec2);

/// Used to help identify our main camera
/// 用于帮助识别我们的主摄像头
#[derive(Component)]
struct MainCamera;

fn setup(mut commands: Commands) {
    // Make sure to add the marker component when you set up your camera
    // 设置相机时,请确保添加标记组件
    commands.spawn((Camera2dBundle::default(), MainCamera));
}

fn my_cursor_system(
    mut mycoords: ResMut<MyWorldCoords>,
    // query to get the window (so we can read the current cursor position)
    // 获取窗口的查询(以便我们可以读取当前光标的位置)
    q_window: Query<&Window, With<PrimaryWindow>>,
    // query to get camera transform
    // 查询以获取相机变换
    q_camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
) {
    // get the camera info and transform
    // 获取相机信息并进行转换
    // assuming there is exactly one main camera entity, so Query::single() is OK
    // 假设只有一个主摄像机实体,那么Query::single()就可以了
    let (camera, camera_transform) = q_camera.single();

    // There is only one primary window, so we can similarly get it from the query:
    // 只有一个主窗口,所以我们可以类似地从查询中获取它:
    let window = q_window.single();

    // check if the cursor is inside the window and get its position
    // 检查光标是否在窗口内并获取它的位置
    // then, ask bevy to convert into world coordinates, and truncate to discard Z
    // 然后,让bevy转换为世界坐标,并截断以丢弃Z
    if let Some(world_position) = window.cursor_position()
        .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor))
        .map(|ray| ray.origin.truncate())
    {
        mycoords.0 = world_position;
        eprintln!("World coords: {}/{}", world_position.x, world_position.y);
    }
}
app.init_resource::<MyWorldCoords>();
app.add_systems(Startup, setup);
app.add_systems(Update, my_cursor_system);

如果您有一个具有多个窗口的更复杂的应用程序,下面是可以处理该问题的更复杂版本的代码:

Code (multi-window version):
use bevy::render::camera::RenderTarget;
use bevy::window::WindowRef;

/// We will add this to each camera we want to compute cursor position for.
/// 我们将把它添加到每个我们想要计算光标位置的相机中。
/// Add the component to the camera that renders to each window.
/// 将组件添加到渲染每个窗口的相机中。
#[derive(Component, Default)]
struct WorldCursorCoords(Vec2);

fn setup_multiwindow(mut commands: Commands) {
    // TODO: set up multiple cameras for multiple windows.
    // TODO:为多个窗口设置多个摄像头。
    // See bevy's example code for how to do that.
    // 参见bevy的示例代码。

    // Make sure we add our component to each camera
    // 确保我们将组件添加到每个摄像头
    commands.spawn((Camera2dBundle::default(), WorldCursorCoords::default()));
}

fn my_cursor_system_multiwindow(
    // query to get the primary window
    // 查询以获取主窗口
    q_window_primary: Query<&Window, With<PrimaryWindow>>,
    // query to get other windows
    // 查询以获取其他窗口
    q_window: Query<&Window>,
    // query to get camera transform
    // 查询以获取相机变换
    mut q_camera: Query<(&Camera, &GlobalTransform, &mut WorldCursorCoords)>,
) {
    for (camera, camera_transform, mut worldcursor) in &mut q_camera {
        // get the window the camera is rendering to
        // 获取相机正在渲染的窗口
        let window = match camera.target {
            // the camera is rendering to the primary window
            // 摄像头正在渲染主窗口
            
            RenderTarget::Window(WindowRef::Primary) => {
                q_window_primary.single()
            },
            // the camera is rendering to some other window
            // 相机正在渲染到其他窗口
            RenderTarget::Window(WindowRef::Entity(e_window)) => {
                q_window.get(e_window).unwrap()
            },
            // the camera is rendering to something else (like a texture), not a window
            // 相机正在渲染其他东西(比如纹理),而不是窗口
            _ => {
                // skip this camera   // 跳过这个相机
                continue;
            }
        };

        // check if the cursor is inside the window and get its position
        // 检查光标是否在窗口内并获取它的位置
        // then, ask bevy to convert into world coordinates, and truncate to discard Z
        // 然后,让bevy转换为世界坐标,并截断以丢弃Z
        if let Some(world_position) = window.cursor_position()
            .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor))
            .map(|ray| ray.origin.truncate())
        {
            worldcursor.0 = world_position;
        }
    }
}
app.add_systems(Startup, setup_multiwindow);
app.add_systems(Update, my_cursor_system_multiwindow);

3D游戏

如果您希望能够检测光标指向的 3D 对象、选择对象等,有一个很好的(非官方)插件: bevy_mod_picking

对于具有平坦地平面的简单自上而下的摄像机视图游戏,仅计算光标下方地面上的坐标可能就足够了。

加载交互示例(见原网页)

在交互式示例中,有一个具有非默认位置和旋转的地平面。有一个红色立方体,使用全局坐标定位,还有一个蓝色立方体,它是地平面的子实体,使用局部坐标定位。它们都应该跟随光标。

Code and explanation:
/// Here we will store the position of the mouse cursor on the 3D ground plane.
/// 这里我们将存储鼠标光标在3D地平面上的位置。
#[derive(Resource, Default)]
struct MyGroundCoords {
    // Global (world-space) coordinates
    // 全局(世界空间)坐标
    global: Vec3,
    // Local (relative to the ground plane) coordinates
    // 局部(相对于地平面)坐标
    local: Vec2,
}

/// Used to help identify our main camera
/// 用于识别主摄像头
#[derive(Component)]
struct MyGameCamera;

/// Used to help identify our ground plane
/// 用于识别地平面
#[derive(Component)]
struct MyGroundPlane;

fn setup_3d_scene(mut commands: Commands) {
    // Make sure to add the marker component when you set up your camera
    // 确保在设置相机时添加标记组件
    commands.spawn((
        MyGameCamera,
        Camera3dBundle {
            // ... your camera configuration ...
            / /……你的相机配置…
            ..default()
        },
    ));
    // Spawn the ground
    // 生成地面
    commands.spawn((
        MyGroundPlane,
        PbrBundle {
            // feel free to change this to rotate/tilt or reposition the ground
            // 可以随意更改它以旋转/倾斜或重新定位地面
            transform: Transform::default(),
            // TODO: set up your mesh / visuals for rendering:
            // TODO:为渲染设置网格/视觉效果:
            // mesh: ...   网格
            // material: ...    材质
            ..default()
        },
    ));
}

fn cursor_to_ground_plane(
    mut mycoords: ResMut<MyGroundCoords>,
    // query to get the window (so we can read the current cursor position)
    // 获取窗口的查询(以便我们可以读取当前光标的位置)
    // (we will only work with the primary window)
    // (我们只使用主窗口)
    q_window: Query<&Window, With<PrimaryWindow>>,
    // query to get camera transform
    // 查询以获取相机变换
    q_camera: Query<(&Camera, &GlobalTransform), With<MyGameCamera>>,
    // query to get ground plane's transform
    // 查询来获取地平面的变换
    q_plane: Query<&GlobalTransform, With<MyGroundPlane>>,
) {
    // get the camera info and transform
    // 获取相机信息并进行转换
    // assuming there is exactly one main camera entity, so Query::single() is OK
    // 假设只有一个主摄像机实体,那么Query::single()就可以了
    let (camera, camera_transform) = q_camera.single();

    // Ditto for the ground plane's transform
    // 对地平面的变换也是如此
    let ground_transform = q_plane.single();

    // There is only one primary window, so we can similarly get it from the query:
    // 只有一个主窗口,所以我们可以类似地从查询中获取它:
    let window = q_window.single();

    // check if the cursor is inside the window and get its position
    // 检查光标是否在窗口内并获取它的位置
    let Some(cursor_position) = window.cursor_position() else {
        // if the cursor is not inside the window, we can't do anything
        // 如果光标不在窗口内,我们什么也做不了
        return;
    };

    // Mathematically, we can represent the ground as an infinite flat plane.
    // 在数学上,我们可以将地面表示为一个无限平面。
    // To do that, we need a point (to position the plane) and a normal vector
    // 为此,我们需要一个点(用于放置平面)和一个法向量
    // (the "up" direction, perpendicular to the ground plane).
    // (向上,垂直于地平面)。

    // We can get the correct values from the ground entity's GlobalTransform
    // 我们可以从地面实体的GlobalTransform中获取正确的值
    let plane_origin = ground_transform.translation();
    let plane_normal = ground_transform.up();

    // Ask Bevy to give us a ray pointing from the viewport (screen) into the world
    // 让Bevy给我们一条从视口(屏幕)指向现实世界的光线
    let Some(ray) = camera.viewport_to_world(camera_transform, cursor_position) else {
        // if it was impossible to compute for whatever reason; we can't do anything
        // 如果由于某种原因无法计算;我们什么都做不了
        return;
    };

    // do a ray-plane intersection test, giving us the distance to the ground
    // 做一个射线平面相交测试,得到到地面的距离
    let Some(distance) = ray.intersect_plane(plane_origin, plane_normal) else {
        // If the ray does not intersect the ground
        // 如果光线不与地面相交
        // (the camera is not looking towards the ground), we can't do anything
        // (摄像头没有朝向地面),我们什么都做不了
        return;
    };

    // use the distance to compute the actual point on the ground in world-space
    // 使用距离来计算现实世界中地面上的实际点
    let global_cursor = ray.get_point(distance);

    mycoords.global = global_cursor;
    eprintln!("Global cursor coords: {}/{}/{}",
        global_cursor.x, global_cursor.y, global_cursor.z
    );

    // to compute the local coordinates, we need the inverse of the plane's transform
    // 为了计算局部坐标,我们需要平面变换的逆
    let inverse_transform_matrix = ground_transform.compute_matrix().inverse();
    let local_cursor = inverse_transform_matrix.transform_point3(global_cursor);

    // we can discard the Y coordinate, because it should always be zero
    // 我们可以放弃Y坐标,因为它应该始终为0
    // (our point is supposed to be on the plane)
    // (我们的点应该在平面上)
    mycoords.local = local_cursor.xz();
    eprintln!("Local cursor coords: {}/{}", local_cursor.x, local_cursor.z);
}
app.init_resource::<MyGroundCoords>();
app.add_systems(Startup, setup_3d_scene);
app.add_systems(Update, cursor_to_ground_plane);

如果地面倾斜/旋转或移动,全局坐标和局部坐标将会不同,并且可能对不同的用例有用,因此我们计算两者。

举一些例子:

  • 如果你想生成一个子实体,或者将坐标量化为网格(对于基于图块的游戏,检测光标下的网格图块),本地坐标会更有用
  • 如果你想在光标的位置生成一些叠加层、粒子效果、其他独立的游戏实体,全局坐标会更有用

非官方 Bevy 作弊书

2.3 定制相机投影

注意:此示例向您展示如何执行 Bevy 未正式支持/认可的操作。风险自负。

具有自定义投影的相机(不使用 Bevy 的标准透视或正交投影之一)。

如果您出于某种原因坚持使用Bevy 的默认坐标系以外的其他坐标系,您也可以使用它来更改坐标系。

在这里,我们实现一个简单的正交投影,它将-1.0到1.0映射到窗口的垂直轴,并尊重窗口的纵横比作为水平轴:

查看 Bevy 如何构建其相机包,以供参考:

此示例基于 2D 相机的设置:

use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::render::primitives::Frustum;
use bevy::render::camera::{Camera, CameraProjection};
use bevy::render::view::VisibleEntities;

#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)]
struct SimpleOrthoProjection {     // 简单正交投影
    near: f32,
    far: f32,
    aspect: f32,
}

impl CameraProjection for SimpleOrthoProjection {
    fn get_projection_matrix(&self) -> Mat4 {
        Mat4::orthographic_rh(
            -self.aspect, self.aspect, -1.0, 1.0, self.near, self.far
        )
    }

    // what to do on window resize
    // 调整窗口大小时要做什么
    fn update(&mut self, width: f32, height: f32) {
        self.aspect = width / height;
    }

    fn far(&self) -> f32 {
        self.far
    }
}

impl Default for SimpleOrthoProjection {
    fn default() -> Self {
        Self { near: 0.0, far: 1000.0, aspect: 1.0 }
    }
}

fn setup(mut commands: Commands) {
    // We need all the components that Bevy's built-in camera bundles would add
    // 我们需要Bevy内置camera bundle所添加的所有组件
    // Refer to the Bevy source code to make sure you do it correctly:
    // 参考Bevy的源代码以确保正确操作:

    // here we show a 2d example
    // 这里我们展示一个2d示例

    let projection = SimpleOrthoProjection::default();

    // position the camera like bevy would do by default for 2D:
    // 像bevy在2D情况下默认做的那样定位相机:
    let transform = Transform::from_xyz(0.0, 0.0, projection.far - 0.1);

    // frustum construction code copied from Bevy
    // 从Bevy中复制截锥构造代码
    let view_projection =
        projection.get_projection_matrix() * transform.compute_matrix().inverse();
    let frustum = Frustum::from_view_projection(
        &view_projection,
        &transform.translation,
        &transform.back(),
        projection.far,
    );

    commands.spawn((
        bevy::render::camera::CameraRenderGraph::new(bevy::core_pipeline::core_2d::graph::NAME),
        projection,
        frustum,
        transform,
        GlobalTransform::default(),
        VisibleEntities::default(),
        Camera::default(),
        Camera2d::default(),
        Tonemapping::Disabled,
    ));
}

fn main() {
    // need to add bevy-internal camera projection management functionality
    // 需要添加bevy-internal相机投影管理功能
    // for our custom projection type
    // 用于自定义投影类型
    use bevy::render::camera::CameraProjectionPlugin;

    App::new()
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup)
        .add_plugin(CameraProjectionPlugin::<SimpleOrthoProjection>::default())
        .run();
}

非官方 Bevy 作弊书

2.4 平移 + 轨道摄像机

此代码由社区贡献。

当前版本由@mirenbharta开发。@skairunner的初始工作。


这是一个类似于 Blender 等 3D 编辑器中的相机控制器。

使用鼠标右键进行旋转,中键进行平移,滚轮进行向内/向外移动。

这主要是为了说明目的而显示的,作为学习的例子。在您的项目中,您可能想尝试 [ bevy_config_cam][project::bevy_config_cam] 插件。

/// Tags an entity as capable of panning and orbiting.
/// 标记一个实体为可平移和绕轨道移动。
#[derive(Component)]
struct PanOrbitCamera {
    /// The "focus point" to orbit around. It is automatically updated when panning the camera
    /// 要环绕的“焦点”。当平移相机时,它会自动更新
    pub focus: Vec3,
    pub radius: f32,
    pub upside_down: bool,
}

impl Default for PanOrbitCamera {
    fn default() -> Self {
        PanOrbitCamera {
            focus: Vec3::ZERO,     // 焦点
            radius: 5.0,           // 半径
            upside_down: false,
        }
    }
}

/// Pan the camera with middle mouse click, zoom with scroll wheel, orbit with right mouse click.
/// 用鼠标中键平移相机,用滚动轮缩放,用鼠标右键绕轨道旋转。
fn pan_orbit_camera(
    windows: Res<Windows>,
    mut ev_motion: EventReader<MouseMotion>,
    mut ev_scroll: EventReader<MouseWheel>,
    input_mouse: Res<Input<MouseButton>>,
    mut query: Query<(&mut PanOrbitCamera, &mut Transform, &Projection)>,
) {
    // change input mapping for orbit and panning here
    // 在这里更改轨道和平移的输入映射
    let orbit_button = MouseButton::Right;
    let pan_button = MouseButton::Middle;

    let mut pan = Vec2::ZERO;
    let mut rotation_move = Vec2::ZERO;
    let mut scroll = 0.0;
    let mut orbit_button_changed = false;

    if input_mouse.pressed(orbit_button) {
        for ev in ev_motion.iter() {
            rotation_move += ev.delta;
        }
    } else if input_mouse.pressed(pan_button) {
        // Pan only if we're not rotating at the moment
        // 仅在当前不旋转时进行平移
        for ev in ev_motion.iter() {
            pan += ev.delta;
        }
    }
    for ev in ev_scroll.iter() {
        scroll += ev.y;
    }
    if input_mouse.just_released(orbit_button) || input_mouse.just_pressed(orbit_button) {
        orbit_button_changed = true;
    }

    for (mut pan_orbit, mut transform, projection) in query.iter_mut() {
        if orbit_button_changed {
            // only check for upside down when orbiting started or ended this frame
            // 只在轨道开始或结束时检查是否颠倒
            // if the camera is "upside" down, panning horizontally would be inverted, so invert the input to make it correct
            // 如果相机是“倒过来”的,水平平移将是反向的,因此反转输入以使其正确
            let up = transform.rotation * Vec3::Y;
            pan_orbit.upside_down = up.y <= 0.0;
        }

        let mut any = false;
        if rotation_move.length_squared() > 0.0 {
            any = true;
            let window = get_primary_window_size(&windows);
            let delta_x = {
                let delta = rotation_move.x / window.x * std::f32::consts::PI * 2.0;
                if pan_orbit.upside_down { -delta } else { delta }
            };
            let delta_y = rotation_move.y / window.y * std::f32::consts::PI;
            let yaw = Quat::from_rotation_y(-delta_x);
            let pitch = Quat::from_rotation_x(-delta_y);
            transform.rotation = yaw * transform.rotation; 
                // rotate around global y axis
                //围绕全局y轴旋转
            transform.rotation = transform.rotation * pitch; 
                // rotate around local x axis
                // 围绕局部x轴旋转
        } else if pan.length_squared() > 0.0 {
            any = true;
            // make panning distance independent of resolution and FOV,
            // 使平移距离不依赖于分辨率和视野,
            let window = get_primary_window_size(&windows);
            if let Projection::Perspective(projection) = projection {
                pan *= Vec2::new(projection.fov * projection.aspect_ratio, projection.fov) / window;
            }
            // translate by local axes
            // 通过局部轴进行平移
            let right = transform.rotation * Vec3::X * -pan.x;
            let up = transform.rotation * Vec3::Y * pan.y;
            // make panning proportional to distance away from focus point
            // 使平移与距焦点的距离成比例
            let translation = (right + up) * pan_orbit.radius;
            pan_orbit.focus += translation;
        } else if scroll.abs() > 0.0 {
            any = true;
            pan_orbit.radius -= scroll * pan_orbit.radius * 0.2;
            // dont allow zoom to reach zero or you get stuck
            // 不要让缩放达到0,否则你会卡住 
            pan_orbit.radius = f32::max(pan_orbit.radius, 0.05);
        }

        if any {
            // emulating parent/child to make the yaw/y-axis rotation behave like a turntable
            // 模拟父/子元素,使偏航/y轴旋转像转盘一样
            // parent = x and y rotation
            // parent = x和y旋转
            // child = z-offset
            // child = z-offset
            let rot_matrix = Mat3::from_quat(transform.rotation);
            transform.translation = pan_orbit.focus + rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, pan_orbit.radius));
        }
    }

    // consume any remaining events, so they don't pile up if we don't need them
    // 消耗所有剩余的事件,这样如果我们不需要它们,它们就不会堆积起来
    // (and also to avoid Bevy warning us about not checking events every frame update)
    // (同时也避免了Bevy警告我们在每一帧更新时不去检查事件)
    ev_motion.clear();
}

fn get_primary_window_size(windows: &Res<Windows>) -> Vec2 {
    let window = windows.get_primary().unwrap();
    let window = Vec2::new(window.width() as f32, window.height() as f32);
    window
}

/// Spawn a camera like this
/// 像这样生成一个相机
fn spawn_camera(mut commands: Commands) {
    let translation = Vec3::new(-2.0, 2.5, 5.0);
    let radius = translation.length();

    commands.spawn((
        Camera3dBundle {
            transform: Transform::from_translation(translation)
                .looking_at(Vec3::ZERO, Vec3::Y),
            ..Default::default()
        },
        PanOrbitCamera {
            radius,
            ..Default::default()
        },
    ));
}

List All Resource Types - Unofficial Bevy Cheat Book
 

非官方 Bevy 作弊书

2.5 列出所有资源类型

此示例演示如何打印已添加为 资源的所有类型的列表。

fn print_resources(world: &World) {
    let components = world.components();

    let mut r: Vec<_> = world
        .storages()
        .resources
        .iter()
        .map(|(id, _)| components.get_info(id).unwrap())
        .map(|info| info.name())
        .collect();

    // sort list alphebetically
    // 按字母顺序排序列表
    r.sort();
    r.iter().for_each(|name| println!("{}", name));
}
// print main world resources
// 打印主要世界资源
app.add_systems(Last, print_resources);

// print render world resources
// 打印渲染世界资源
app.sub_app_mut(RenderApp)
    .add_systems(Render, print_resources.in_set(RenderSet::Render));

它列出了当前存在于您的ECS World中的所有资源的类型(所有注册的插件、您自己的插件等)。

请注意,这不会您提供可用作资源的每种类型的列表。为此,您应该查阅 API 文档,寻找该Resource特征的实现者。

请参阅此处了解 Bevy 中提供的类型的摘要。


Bevy Setup Tips - Unofficial Bevy Cheat Book
 

非官方 Bevy 作弊书

3 bevy设置技巧

本章收集了有关配置项目或开发工具的其他技巧,这些技巧是从 Bevy 社区收集的,超出了 Bevy官方设置文档中涵盖的内容。

请随意建议本章下添加的内容。


另请参阅本书中的以下其他相关内容:

非官方 Bevy 作弊书

3.1 入门

本页介绍了 Bevy 开发所需的基本设置。


在大多数情况下,Bevy 与任何其他 Rust 库一样。您需要安装 Rust 并设置您的开发环境,就像任何其他 Rust 项目一样。您可以使用Rustup安装 Rust 。请参阅 Rust 的官方设置页面

在Linux上,您需要一些系统库的开发文件。请参阅 官方 Bevy Linux 依赖项页面

另请参阅官方 Bevy 手册 和官方 Bevy 自述文件中的设置页面。

创建一个新项目

您可以简单地从 IDE/编辑器或命令行创建一个新的 Rust 项目:

cargo new --bin my_game

(创建一个名为 my_game 的项目)

Cargo.toml文件包含项目的所有配置。添加最新版本bevy作为依赖项。您的文件现在应该如下所示:

[package]
name = "my_game"
version = "0.1.0"
edition = "2021"

[dependencies]
bevy = "0.12"

src/main.rs文件是您的主要源代码文件。这是您开始编写 Rust 代码的地方。对于一个最小的 Bevy应用程序,您至少需要以下内容:

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .run();
}

您现在可以编译并运行您的项目。第一次,这将需要一段时间,因为它需要构建整个 Bevy 引擎和依赖项。随后的运行应该很快。您可以从 IDE/编辑器或命令行执行此操作:

cargo run

文档

您可以在一处生成您自己的文档(如docs.rs上的文档)以供离线使用,包括您自己项目中的所有内容和所有依赖项。

cargo doc --open

这将构建所有 HTML 文档并在您的 Web 浏览器中打开它们。

它不需要互联网连接,并为您提供了一种简单的方法来一次搜索 API 文档以查找依赖关系树中的所有包。它比文档的在线版本更有用。

可选的额外设置

使用默认的 Rust 未优化的开发版本,您可能很快就会遇到性能缓慢到无法使用的情况。请参阅此处如何修复。

迭代重新编译速度对于保持您的工作效率非常重要,因此每次您想要测试游戏时,您不必等待 Rust 编译器重建您的项目。Bevy 的入门页面 提供了有关如何加快编译时间的建议。

另请参阅“开发工具和编辑器”页面,了解有关可能有帮助的其他外部开发工具的建议。

下一步是什么?

看看这本书的指导教程页面,以及 Bevy 的官方示例

查看Bevy Assets 网站,查找来自社区的其他教程和学习资源,以及可在您的项目中使用的插件 。

加入Discord社区与我们聊天!

遇到问题?

如果有些东西不起作用,请务必查看“常见陷阱”一章,看看这本书是否对您有帮助。此处记录了 Bevy 社区成员遇到的一些最常见问题的解决方案。

如果您需要帮助,请使用GitHub Discussions,或者欢迎在Discord中聊天并寻求帮助。

显卡驱动程序

为了发挥最佳性能,Bevy 需要 DirectX 12 (Windows) 或 Vulkan(Linux、Android、Windows)。macOS/iOS 应该可以使 Metal 正常工作,无需任何特殊的驱动程序设置。

OpenGL (GLES3) 可以用作后备,但可能会出现问题(一些错误、不受支持的功能、性能较差)。

确保您的系统上安装了兼容的硬件和驱动程序。您的用户还需要满足此要求。

如果 Bevy 无法正常工作,请为您的操作系统安装最新的驱动程序,或者检查您的 Linux 发行版是否需要安装 Vulkan 附加软件包。

支持网页游戏,并且可以在任何使用 WebGL2 的现代浏览器中运行。性能有限,某些 Bevy 功能​​将无法使用。还支持新的实验性高性能 WebGPU API,但浏览器的采用仍然有限。

非官方 Bevy 作弊书

3.2 文本编辑器/IDE

本子章节包含针对不同文本编辑器和 IDE 的提示。

Bevy 在很大程度上与任何其他 Rust 项目一样。如果您的编辑器/IDE 是针对 Rust 设置的,那么这可能就是您所需要的。本子章节包含可能对 Bevy 特别有用的附加信息。

如果您对您选择的编辑器有任何提示/建议/配置,并希望与社区分享,请创建 GitHub Issue,以便我们将其添加到书中。如果您的编辑器不在列表中,我会添加它。

非官方 Bevy 作弊书

3.2.1 Visual Studio Code

如果您是 VSCode 用户并且希望向此页面添加一些内容,请提交GitHub Issue

Rust 语言支持

为了获得良好的 Rust 支持,请安装 Rust 分析器插件(Rust Analyzer plugin)。

加速 Rust 分析

如果您曾经设置.cargo/config.toml使用非默认链接器以进行快速编译,Rust 分析器将不幸地忽略它。您还需要使用以下设置配置 RA 才能使用它(在 VSCode 中settings.json):

Windows:

"rust-analyzer.cargo.extraEnv": {
    "RUSTFLAGS": "-Clinker=rust-lld.exe"
}

Linux (mold):

"rust-analyzer.cargo.extraEnv": {
    "RUSTFLAGS": "-Clinker=clang -Clink-arg=-fuse-ld=mold"
}

Linux(LLD):

"rust-analyzer.cargo.extraEnv": {
    "RUSTFLAGS": "-Clinker=clang -Clink-arg=-fuse-ld=lld"
}

CARGO_MANIFEST_DIR

运行应用程序/游戏时,Bevy将在Bevy_ASSET_ROOT或CARGO_MANIFEST_DIR环境变量中指定的路径中搜索资产文件夹。这使得cargo run可以从终端上正常运行。

如果您想以非标准方式(例如,在调试器内)从 VSCode 运行项目,则必须确保正确设置。

如果未设置,Bevy 将在可执行二进制文件所在的同一文件夹中搜索assets。这使得分发变得容易。然而,在开发过程中,由于您的可执行文件存在于暂存文件的target目录中,Bevy 将无法找到assets.

下面的代码片段显示了如何创建用于调试 Bevy 的运行配置(使用lldb):

(这是为了 Bevy 本身的开发,并用breakout示例进行测试)

(如果用于您的项目,请按照您的需求调整)

{
    "type": "lldb",
    "request": "launch",
    "name": "Debug example 'breakout'",
    "cargo": {
        "args": [
            "build",
            "--example=breakout",
            "--package=bevy"
        ],
        "filter": {
            "name": "breakout",
            "kind": "example"
        }
    },
    "args": [],
    "cwd": "${workspaceFolder}",
    "env": {
        "CARGO_MANIFEST_DIR": "${workspaceFolder}",
    }
}

为了支持动态链接,您还应该在“ env”部分中添加以下内容

Linux:

"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:HOME}/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib",

(如果您使用不同的工具链/架构,请替换stable-x86_64-unknown-linux-gnu

Windows::我不知道。如果您这样做,请提出问题


JetBrains (RustRover, IntelliJ, CLion) - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

3.2.2 JetBrains(RustRover、IntelliJ、CLion)

如果您是 JetBrains 用户并且希望向此页面添加一些内容,请提交GitHub Issue

Rust 语言支持

使用查询时,由于 Bevy 依赖过程宏,类型信息会丢失。您可以通过在 IDE 中启用过程宏支持来解决此问题。

  1. 在“帮助”对话框中进行“查找操作”,键入实验性特征 Experimental feature
  2. 启用功能org.rust.cargo.evaluate.build.scripts 和 org.rust.macros.proc

Kakoune - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

3.2.3 Kakoune

如果您是 Kakoune 用户并且希望向此页面添加一些内容,请提交GitHub Issue

Rust 语言支持

您可以将kak-lsprust-analyzer一起使用。

您只需要安装 RA 服务器,而不需要官方的 VSCode 插件。

你可以通过 rustup 管理它

rustup component add rust-analyzer

或者你可以从 git 自己构建/安装它:

git clone https://github.com/rust-lang/rust-analyzer
cd rust-analyzer
git checkout release # use the `release` branch instead of `main`
cargo xtask install --server

设置 kak-lsp 最简单的方法是使用 plug.kak。

如果没有plug.kak,请将以下内容放入~/.config/kak/kakrc

evaluate-commands %sh{
    plugins="$kak_config/plugins"
    mkdir -p "$plugins"
    [ ! -e "$plugins/plug.kak" ] && \
        git clone -q https://github.com/andreyorst/plug.kak.git "$plugins/plug.kak"
    printf "%s\n" "source '$plugins/plug.kak/rc/plug.kak'"
}
plug "andreyorst/plug.kak" noload

然后用 Rust 支持设置 kak-lsp:

plug "kak-lsp/kak-lsp" do %{
    cargo install --force --path .
} config %{
    set global lsp_cmd "kak-lsp -s %val{session}"

    # create a command to let you restart LSP if anything goes wrong / gets glitched
    define-command lsp-restart -docstring 'restart lsp server' %{ lsp-stop; lsp-start }

    # helper command to enable LSP
    define-command -hidden lsp-init %{
        lsp-enable-window
        # preferences:
        set window lsp_auto_highlight_references true
        lsp-auto-signature-help-enable
        # keybind: use "," to get a menu of available LSP commands
        map global normal "," ": enter-user-mode lsp<ret>" -docstring "LSP mode"
    }

    hook global KakEnd .* lsp-exit

    # autoenable LSP when opening Rust files
    hook global WinSetOption filetype=rust %{
        lsp-init
    }
}
# formatting settings for Rust files
hook global BufSetOption filetype=rust %{
    set buffer tabstop 4
    set buffer indentwidth 4
    set buffer formatcmd 'rustfmt'
    set buffer autowrap_column 100
    expandtab
}

在 ~/. config/kak-lsp/kak-lsp. toml 中放入以下内容以使用rust-analyzer

[server]
# Shut down the `rust-analyzer` process after a period of inactivity
timeout = 900

[language.rust]
filetypes = ["rust"]
roots = ["Cargo.toml"]
command = "rust-analyzer"
settings_section = "rust-analyzer"

[language.rust.settings.rust-analyzer]
# Proc Macro support is important for Bevy projects
procMacro.enable = true
# disable hover actions, can be laggy on complex projects like Bevy
hoverActions.enable = false
# use the data generated by `cargo check`; disable if it is too slow for you
cargo.loadOutDirsFromCheck = true

Vim - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

3.2.4 Vim

如果您是 Vim 用户并且希望向此页面添加一些内容,请提交GitHub Issue

 

Emacs - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

3.2.5 Emacs

如果您是 Emacs 用户并且希望向此页面添加一些内容,请提交GitHub Issue

非官方 Bevy 作弊书

3.3 配置bevy(功能、模块化等)

Bevy 非常模块化且可配置。它被实现为许多单独的cargo crates,允许您移除不需要的部件。较高级别的功能构建在较低级别的基础包之上,可以禁用或用替代方案替换。

较低级别的核心包(如 Bevy ECS)也可以完全独立使用,或集成到其他非 Bevy 项目中。

Bevy Cargo Features

在 Bevy 项目中,您可以用cargo features 启用/禁用 Bevy 的各个部分。

默认情况下启用许多常用功能。如果您想禁用其中一些,则需要禁用所有这些并重新启用您需要的那些。不幸的是,Cargo 不允许您禁用单个默认功能。

以下是您可以如何配置 Bevy:

[dependencies.bevy]
version = "0.12"
# Disable the default features if there are any that you do not want
# 禁用默认功能(如果有不需要的功能)
default-features = false
features = [
  # These are the default features:
  # 这些是默认功能:
  # (re-enable whichever you like)
  # (根据你的喜好重新启用)

  # Bevy functionality:   # Bevy功能:
  "multi-threaded",     # Run with multithreading   # 多线程运行
  "bevy_asset",         # Assets management   # 资产管理
  "bevy_audio",         # Builtin audio   # 内置音频
  "bevy_gilrs",         # Gamepad input support    # 手柄输入支持
  "bevy_scene",         # Scenes management   # 场景管理
  "bevy_winit",         # Window management (cross-platform Winit backend)
                        # 窗口管理(跨平台Winit后端)
  "bevy_render",        # Rendering framework core   # 渲染框架核心
  "bevy_core_pipeline", # Common rendering abstractions   # 常见的渲染抽象
  "bevy_gizmos",        # Support drawing debug lines and shapes
                        # 支持绘制调试线条和形状
  "bevy_sprite",        # 2D (sprites) rendering   # 2D (sprites)渲染
  "bevy_pbr",           # 3D (physically-based) rendering    # 3D(基于物理的)渲染
  "bevy_gltf",          # GLTF 3D assets format support   # GLTF 3D资产格式支持
  "bevy_text",          # Text/font rendering   # 文本/字体渲染
  "bevy_ui",            # UI toolkit   # UI工具包
  "animation",          # Animation support   # 动画支持
  "tonemapping_luts",   # Support different camera Tonemapping modes (enables KTX2+zstd)
                        # 支持不同的相机色调映射模式(启用KTX2+zstd)
  "default_font",       # Embed a minimal default font for text/UI    
                        # 为text/UI嵌入最小的默认字体

  # File formats:   # 文件格式:
  "png",    # PNG image format for simple 2D images   # 简单2D图像的png图像格式
  "hdr",    # HDR images    # hdr图像
  "ktx2",   # Preferred format for GPU textures    # GPU纹理的首选格式
  "zstd",   # ZSTD compression support in KTX2 files    # KTX2文件中zstd压缩支持
  "vorbis", # Audio: OGG Vorbis   # 音频:OGG vorbis

  # Platform-specific:   # 特定于平台:
  "x11",                   # Linux: Support X11 windowing system   # Linux:支持x11窗口系统
  "android_shared_stdcxx", # Android: use shared C++ library   # Android:使用共享c++库
  "webgl2",                # Web: use WebGL2 instead of WebGPU   
                           # Web:使用webgl2而不是WebGPU

  # These are other (non-default) features that may be of interest:
  # 这些是其他可能感兴趣的(非默认)特性:
  # (add any of these that you need)
  # (添加你需要的内容)

  # Bevy functionality:   # Bevy功能:
  "asset_processor",      # Asset processing   # 资产处理
  "file_watcher",         # Asset hot-reloading   # 资源热加载
  "subpixel_glyph_atlas", # Subpixel antialiasing for text/fonts
                          # 文本/字体的亚像素反走样
  "serialize",            # Support for `serde` Serialize/Deserialize
                          # 支持`serde`的序列化/反序列化
  "async-io",             # Make bevy use `async-io` instead of `futures-lite`
                          # 让bevy使用`async-io`代替`futures-lite`
  "pbr_transmission_textures", # Enable Transmission textures in PBR materials
                               # 在PBR材质中启用传输纹理
                               # (may cause issues on old/lowend GPUs)
                               # (在旧/低端gpu上可能会导致问题)

  # File formats:
  "dds",  # Alternative DirectX format for GPU textures, instead of KTX2
          # 替代KTX2的GPU纹理DirectX格式
  "jpeg", # JPEG lossy format for 2D photos    # 2D照片jpeg有损格式
  "webp", # WebP image format    # webp图像格式
  "bmp",  # Uncompressed BMP image format   # 未压缩的bmp图像格式
  "tga",  # Truevision Targa image format   # Truevision Targa图像格式
  "exr",  # OpenEXR advanced image format   # OpenEXR高级图像格式
  "pnm",  # PNM (pam, pbm, pgm, ppm) image format    # pnm (pam, pbm, pgm, ppm)图像格式
  "basis-universal", # Basis Universal GPU texture compression format
                     # Basis通用GPU纹理压缩格式
  "zlib", # zlib compression support in KTX2 files   # KTX2文件对zlib压缩的支持
  "flac", # Audio: FLAC lossless format   # 音频:flac无损格式
  "mp3",  # Audio: MP3 format (not recommended)   # Audio: mp3格式(不推荐)
  "wav",  # Audio: Uncompressed WAV   # Audio:未压缩的wav
  "symphonia-all", # All Audio formats supported by the Symphonia library
                   # Symphonia库支持的所有音频格式
  "shader_format_glsl", # GLSL shader support   # GLSL着色器支持
  "shader_format_spirv", # SPIR-V shader support   # spirv着色器支持

  # Platform-specific:   # 特定于平台
  "wayland",              # (Linux) Support Wayland windowing system
                          # (Linux)支持wayland视窗系统
  "accesskit_unix",       # (Unix-like) AccessKit integration for UI Accessibility
                          # (类unix)集成UI访问工具包
  "bevy_dynamic_plugin",  # (Desktop) support for loading of `DynamicPlugin`s
                          # (桌面)对DynamicPlugin加载的支持

  # Development/Debug features:   # 开发/调试功能:
  "dynamic_linking",   # Dynamic linking for faster compile-times
                       # 加快编译时间的动态链接
  "trace",             # Enable tracing for performance measurement
                       # 启用性能测量跟踪
  "detailed_trace",    # Make traces more verbose   # 让跟踪更详细
  "trace_tracy",       # Tracing using `tracy`   # 使用`tracy`进行追踪
  "trace_tracy_memory", # + memory profiling   # +内存分析
  "trace_chrome",      # Tracing using the Chrome format   # 使用Chrome格式跟踪
  "wgpu_trace",        # WGPU/rendering tracing   # WGPU/渲染跟踪
  "debug_glam_assert", # Assertions to validate math (glam) usage
                       # 验证数学(glam)使用的断言
  "embedded_watcher",  # Hot-reloading for Bevy's internal/builtin assets
                       # 热加载Bevy的内部/内置资源
]
(有关 Bevy 货物功能的完整列表,请参阅此处。)

图形/渲染 Graphics / Rendering

对于图形应用程序或游戏(大多数 Bevy 项目),您可以包含 bevy_winit并选择渲染功能。对于 Linux支持,您至少需要  x11 或 wayland 之一。

bevy_renderbevy_core_pipeline对于任何使用 Bevy 渲染的应用程序都是必需的。

如果您只需要 2D 而不需要 3D,请添加 bevy_sprite.

如果您只需要 3D 而不需要 2D,请添加 bevy_pbr. 如果您要从GLTF 文件加载 3D 模型,请添加bevy_gltf.

如果您使用 Bevy UI,则需要bevy_textbevy_uidefault_font 嵌入一​​个简单的字体文件,这对于原型设计很有用,因此您不需要在项目中拥有字体资源。在真实的项目中,您可能想使用自己的字体,这样您的文本就可以与游戏的艺术风格完美搭配。在这种情况下,您可以禁用该default_font功能。

如果您想在屏幕上绘制调试线条和形状,请添加bevy_gizmos.

如果您不需要任何图形(例如专用游戏服务器、科学模拟等),您可以删除所有这些功能。

文件格式 File Formats 

您可以使用相关的cargo features来启用/禁用对加载各种不同文件格式的资产的支持。

浏览此处获取更多信息。

输入设备 Input Devices 

如果您不关心游戏手柄(控制器/操纵杆) 支持,您可以禁用bevy_gilrs.

特定于平台 Platform-specific

Linux 窗口化后端

Linux上,您可以选择支持 X11、Wayland 或两者。仅x11默认情况下启用,因为它是遗留系统,应该与大多数/所有发行版兼容,以使您的构建更小并编译更快。您可能还需要另外启用wayland, 以完全且原生地支持现代 Linux 环境。这将为您的项目添加一些额外的传递依赖项。

一些 Linux 发行版或平台可能会在 X11 上遇到困难,但在 Wayland 上工作得更好。您应该启用两者以获得最佳兼容性。

WebGPU 与 WebGL2

在 [Web/WASM][platform::web] 之间,您可以在这两个渲染后端之间进行选择。

WebGPU 是现代实验性解决方案,提供良好的性能和完整的功能支持,但浏览器对它的支持有限(仅在最新版本的 Chrome 和 Firefox nightly 中工作)。

WebGL2 提供了与所有浏览器的最佳兼容性,但性能较差,并且对可以在 Bevy 中使用的图形功能类型有一些限制。

如果启用,cargo feature 会选择 WebGL2功能。如果禁用,则使用 WebGPU。

发展特点

当您开发项目时,这些功能可能很有用:

资产热重载和处理 Asset hot-reloading and processing

file_watcher功能支持桌面平台上支持的资源热重载

asset_processor功能支持资产处理,允许您在开发过程中自动转换和优化资产。

动态链接 Dynamic Linking

dynamic_linking导致 Bevy 被构建并链接为共享/动态库。这将使开发过程中的重新编译速度更快

仅桌面平台支持此功能。众所周知,它在 Linux 上运行得很好。Windows 和 macOS 也受支持,但测试较少,并且过去曾出现过问题。

不建议为您打算发布给其他人的发布版本启用此功能,除非您有充分的特殊理由并且您知道自己在做什么。它引入了不必要的复杂性(您需要捆绑额外的文件)以及可能无法正常工作的可能性。您应该只在开发期间使用它。

因此,将该功能指定为cargo 的命令行选项可能会更方便,而不是将其放入您的Cargo.toml. 只需像这样运行您的项目:

cargo run --features bevy/dynamic_linking

您还可以将其添加到您的IDE/编辑器配置中。

追踪 Tracing

这些 trace 和 wgpu_trace 功能可能对于分析和诊断性能问题很有用。

trace_chrome 和 trace_tracy选择您想要用于可视化跟踪的后端。

请参阅Bevy 有关分析的官方文档以了解更多信息。

非官方 Bevy 作弊书

3.4 社区插件生态系统

Bevy 的非官方社区制作插件生态系统正在不断发展。它们提供了许多未正式包含在引擎中的功能。在您的项目中使用其中一些可能会让您受益匪浅。

要找到此类插件,您应该在 Bevy 官方网站上搜索Bevy Assets页面。这是 Bevy 已知社区制作的东西的官方注册表。如果您发布自己的 Bevy 插件,您应该贡献一个链接以添加到该页面

请注意,某些第 3 方插件可能使用不寻常的许可证!在项目中使用插件之前,请务必检查许可证。


本书中的其他页面提供了使用第 3 方插件时有价值的信息:


Dev Tools and Editors for Bevy - Unofficial Bevy Cheat Book

非官方 Bevy 作弊书

3.5 Bevy 的开发工具和编辑器

Bevy 还没有官方编辑器或其他此类工具。官方编辑器被计划作为未来的长期目标。与此同时,这里有一些社区制作的工具可以帮助您。


编辑器

bevy_inspector_egui在游戏中为您提供一个简单的类似编辑器的属性检查器窗口。它允许您在游戏运行时实时修改组件和资源的值。

bevy_editor_pls是一个类似编辑器的界面,您可以将其嵌入到游戏中。它具有更多功能,例如切换应用程序状态、飞行相机、性能诊断和检查器面板。

space_editor是另一个可以嵌入到您的游戏中的此类编辑器。它似乎是为受 Unity 启发的预制工作流程而设计的。

您还可以通过将场景导出到GLTF,也可以将Blender用作关卡/场景编辑器。Blender Bevy 组件工作流程项目改进了这种体验,允许您在 Blender 中设置 Bevy ECS组件 ,将它们包含在导出的 GLTF 中,并在 Bevy 中使用它们。

诊断 Diagnostics

bevy_mod_debugdump是一个帮助可视化您的应用程序计划(所有已注册的 系统及其排序依赖项)和 Bevy 渲染图的工具。

非官方 Bevy 作弊书

3.6 性能可调

Bevy 提供了许多在大多数情况下可以提高性能的功能,并且其中大多数功能都是默认启用的。然而,它们可能对某些项目有害。

幸运的是,其中大多数都是可配置的。大多数用户可能不应该触及这些设置,但如果您的游戏在 Bevy 的默认配置下运行不佳,此页面将向您显示一些您可以尝试更改的内容,看看它们是否对您的项目有帮助。

Bevy 的默认配置在设计时就考虑到了可扩展性。也就是说,当您向项目添加更多功能和复杂性时,您不必过多担心性能。Bevy 会自动注意分配工作负载,以充分利用可用的硬件(GPU、CPU 多线程)。

然而,它可能会损害更简单的项目,或者在某些情况下产生不良影响。

这种权衡是好的,因为即使有额外的开销,小型和简单的游戏无论如何也可能足够快,但大型和复杂的游戏将受益于高级调度以避免瓶颈。当您添加更多内容时,您可以开发游戏而不会大幅降低性能。

多线程开销

Bevy 拥有智能多线程执行器,因此您的系统可以在不需要对相同数据进行冲突访问时自动在多个 CPU 核心上并行运行,同时遵守排序约束。这太棒了,因为您可以不断添加更多系统来执行不同的操作并在游戏中实现更多功能,而 Bevy 将充分利用现代多核 CPU,而无需您付出任何努力!

然而,智能调度给所有常见操作(例如每次系统运行时)增加了一些开销。在每帧几乎没有工作要做的项目中,特别是如果所有系统都很快完成时,开销加起来可能会掩盖您正在做的实际有用工作!

您可能想尝试禁用多线程,看看如果没有它,您的游戏是否会表现得更好。

仅针对更新计划禁用多线程

可以按计划禁用多线程。这意味着很容易仅针对您的代码/游戏逻辑(在时间表中Update)禁用它,同时仍然在所有 Bevy 引擎内部系统启用它。

这可以加速没有太多游戏逻辑的简单游戏,同时仍然让引擎以多线程运行。

您可以通过应用程序生成器编辑特定计划的设置:

use bevy::ecs::schedule::ExecutorKind;

App::new()
    .add_plugins(DefaultPlugins)
    .edit_schedule(Update, |schedule| {
        schedule.set_executor_kind(ExecutorKind::SingleThreaded);
    })
    // ...

完全禁用多线程

如果您想尝试完全禁用所有内容的多线程,可以通过删除multi-threaded默认的 Cargo 功能来实现。

Cargo.toml

[dependencies.bevy]
version = "0.12"
default-features = false
features = [
   # re-enable everything you need, without `multi-threaded`
   # 重新启用所需的一切,而不使用“多线程”`
]

(请参阅此处了解如何配置 Bevy 的 cargo features

一般不建议这样做。Bevy 设计用于多线程工作。仅当您确实需要时才考虑它(例如,如果您正在对项目进行特殊构建以在有意义的系统上运行,例如 WASM 或旧硬件)。

多线程配置

您可以配置 Bevy 使用的 CPU 线程数。

Bevy 创建线程有 3 个不同的目的:

  • 计算:所有系统和所有每帧工作的运行位置
  • AsyncCompute:用于独立于帧速率的后台处理
  • I/O:用于加载资产和其他磁盘/网络活动

默认情况下,Bevy 按如下方式分割/分区可用的 CPU 线程:

  • I/O:25% 的可用 CPU 线程,最少 1 个,最多 4 个
  • AsyncCompute:25% 的可用 CPU 线程,最少 1 个,最多 4 个
  • 计算:所有剩余线程

这意味着没有过度配置。每个硬件 CPU 线程都用于一个特定目的。

这为混合 CPU 工作负载提供了良好的平衡。特别是对于加载大量资源的游戏(特别是在游戏过程中动态加载资源时),专用 I/O 线程将减少卡顿和加载时间。后台计算不会影响您的帧速率。

例子:

CPU 核心/线程# 输入/输出# 异步计算# 计算
1-3111
4112
6222
8224
10334
12336
16448
244416
324424

注意:Bevy 目前没有对非对称(big.LITTLE 大.小 或 Intel P/E 内核)CPU 进行任何特殊处理。在理想的情况下,也许使用大/P 核心的数量进行计算且使用小/E 核心的数量进行I/O 会更好。

过度配置

但是,如果您的游戏执行的 I/O(资源加载)或后台计算非常少,则此默认配置可能是次优配置。这些线程大部分时间都会处于闲置状态。同时,计算是你的帧更新循环,对游戏的整体帧速率很重要,它只限于更少的线程。这对于内核较少(线程总数少于 4 个)的 CPU 来说尤其糟糕。

例如,在我的项目中,我通常在加载屏幕期间加载所有资源,因此在正常游戏过程中不会使用 I/O 线程。我很少使用AsyncCompute。

如果您的游戏是这样的,您可能希望所有 CPU 线程都可用于计算。这可以提高你的帧速率,特别是在内核较少的 CPU 上。但是,游戏过程中的任何 AsyncCompute 或 I/O 工作负载都可能会影响游戏的性能/帧速率一致性。

以下是如何做到这一点:

use bevy::core::TaskPoolThreadAssignmentPolicy;
use bevy::tasks::available_parallelism;

App::new()
    .add_plugins(DefaultPlugins.set(TaskPoolPlugin {
        task_pool_options: TaskPoolOptions {
            compute: TaskPoolThreadAssignmentPolicy {
                // set the minimum # of compute threads
                // 设置计算线程的最小数量
                // to the total number of available threads
                // 可用线程总数
                min_threads: available_parallelism(),
                max_threads: std::usize::MAX, 
                   // unlimited max threads
                   // 不限制最大线程数
                percent: 1.0, 
                   // this value is irrelevant in this case
                   // 这个值在这里是不相关的
            },
            // keep the defaults for everything else
            // 保留其他所有内容的默认值
            ..default()
        }
    }))
    // ...

这是一个完全自定义配置的示例:

App::new()
    .add_plugins(DefaultPlugins.set(TaskPoolPlugin {
        task_pool_options: TaskPoolOptions {
            min_total_threads: 1,
            max_total_threads: std::usize::MAX, 
                // unlimited threads
                // 无限制的线程
            io: TaskPoolThreadAssignmentPolicy {
                // say we know our app is i/o intensive (asset streaming?)
                // 假设我们知道我们的应用程序是I/O密集型的(资产流?)
                // so maybe we want lots of i/o threads
                // 所以我们可能需要很多I/O线程
                min_threads: 4,
                max_threads: std::usize::MAX,
                percent: 0.5, 
                  // use 50% of available threads for I/O
                  // 使用50%的可用线程进行I/O
            },
            async_compute: TaskPoolThreadAssignmentPolicy {
                // say our app never does any background compute,
                // 假设我们的应用程序从不进行后台计算,
                // so we don't care, but keep one thread just in case
                // 所以我们不关心,但保留一个线程以防万一
                min_threads: 1,
                max_threads: 1,
                percent: 0.0,
            },
            compute: TaskPoolThreadAssignmentPolicy {
                // say we want to use at least half the CPU for compute
                // 假设我们希望至少使用一半的CPU进行计算
                // (maybe over-provisioning if there are very few cores)
                // (如果内核数量很少,可能会过度配置)
                min_threads: available_parallelism() / 2,
                // but limit it to a maximum of 8 threads
                // 但限制为最多8个线程
                max_threads: 8,
                // 1.0 in this case means "use all remaining threads"
                // 这里的1.0表示“使用所有剩余线程”
                // (that were not assigned to io/async_compute)
                // (没有赋值给io/async_compute)
                // (clamped to min_threads..=max_threads)
                // (限定为min_threads..=max_threads)
                percent: 1.0,
            },
        }
    }))
    // ...

流水线渲染 Pipelined Rendering

Bevy 有一个流水线渲染架构。这意味着 Bevy 的 GPU 相关系统(在 CPU 上运行,为每一帧的 GPU 准备工作)将与下一帧的所有正常系统并行运行。Bevy 将与下一帧更新并行渲染前一帧。

通过更好地利用 CPU 多线程,这将提高 GPU 利用率(使 GPU 不太可能闲置等待 CPU 分配工作)。通常,它可以使帧速率提高 10-30%,有时甚至更高。

然而,它也会影响感知的输入延迟(“点击到光子”延迟),通常会变得更糟。玩家输入的效果可能会延迟一帧显示在屏幕上。它可能会通过更快的帧速率得到补偿,也可能不会。这是一个图表,可以直观地看到所发生的情况:

比较流水线和非流水线渲染的时间线。 在流水线情况下,在屏幕上看到鼠标单击的效果之前,会显示一帧附加内容。

实际的鼠标单击发生在帧之间。在这两种情况下,第 4 帧都是 Bevy 检测到输入的时间。在流水线情况下,前一帧的渲染是并行完成的,因此屏幕上会出现没有输入的附加帧。

如果没有流水线,用户将看到他们的输入延迟 1 帧。如果使用流水线,则会延迟 2 帧

然而,在上图中,流水线带来的帧速率增加足够大,总体上输入可以更快地处理和显示。你的申请可能就没那么幸运了。


如果您更关心延迟而不是帧速率,则可能需要禁用管道渲染。为了获得最佳延迟,您可能还想 禁用 VSync

以下是禁用管道渲染的方法:

use bevy::render::pipelined_rendering::PipelinedRenderingPlugin;

App::new()
    .add_plugins(DefaultPlugins.build().disable::<PipelinedRenderingPlugin>())
    // ...
    .run();

集群前向渲染

默认情况下,Bevy 使用集群前向渲染架构进行 3D。视口(显示游戏的屏幕区域)被分成矩形/体素,以便可以单独处理场景的每个小部分的照明。这允许您在 3D 场景中使用许多灯光,而不会影响性能。

这些簇的尺寸会影响渲染性能。默认设置适用于大多数 3D 游戏,但根据您的游戏对其进行微调可以提高性能。

在具有自上而下视角相机的游戏中(例如许多策略和模拟游戏),大多数灯光与相机的距离往往相似。在这种情况下,您可能希望减少 Z 切片的数量(以便将屏幕分割成更小的 X/Y 矩形,但每个矩形覆盖更多的距离/深度):

use bevy::pbr::ClusterConfig;

commands.spawn((
    Camera3dBundle {
        // ... your 3D camera configruation
        // ……你的3D相机配置
        ..Default::default()
    },
    ClusterConfig::FixedZ {
        // 4096 clusters is the Bevy default
        // 4096个集群是Bevy的默认值
        // if you don't have many lights, you can reduce this value
        // 如果你没有很多光源,你可以减小这个值
        total: 4096,
        // Bevy default is 24 Z-slices
        // Bevy默认是24个z切片
        // For a top-down-view game, 1 is probably optimal.
        // 对于俯视图游戏,1可能是最佳选择。
        z_slices: 1,
        dynamic_resizing: true,
        z_config: Default::default(),
    }
));

对于使用很少灯光的游戏,或者灯光影响整个场景的游戏(例如在小房间/室内区域内),您可能需要尝试禁用集群:

commands.spawn((
    Camera3dBundle {
        // ... your 3D camera configruation
        // ...您的3D相机配置
        ..Default::default()
    },
    ClusterConfig::Single,
));

除了上述特定场景之外,更改这些设置可能会导致许多游戏性能下降。

非官方 Bevy 作弊书

3.7 使用前沿 Bevy(bevy main)

Bevy 的开发进展非常快,并且经常有令人兴奋的新事物尚未发布。此页面将为您提供有关使用 bevy 开发版本的建议。

快速开始

如果您使用任何第三方插件而只想使用 bevy 主开发分支:

[dependencies]
bevy = { git = "https://github.com/bevyengine/bevy" }

但是,如果您正在使用外部插件,则应该阅读本页的其余部分。您可能需要做更多的事情才能使一切兼容。

您应该使用最新的 Bevy 吗?您应该使用哪个版本的 Bevy?

bevy遵循“火车发布”模式,期限宽松。每 3 个月准备一个新的主要版本,其中将包含自上一个版本以来的所有新开发(功能、修复等)。发布日期并不严格,通常会推迟几周以解决未解决的问题。

此外,Bevy 通常会根据需要在每个主要版本之后发布一两个补丁版本,以修复发布后不久发现的任何错误。它不会包含所有修复,仅包含被认为足够重要的小型非破坏性内容。

大多数 Bevy 项目应该使用 crates.io 上的最新版本。如果您想安全起见,可以等到第一个补丁发布 ( 0.*.1),然后再升级到新的主要版本。您可能还需要等待您正在使用的任何第三方插件来支持新的 Bevy 版本。

另一方面,为了进行实验和 Bevy 开发,我们鼓励您尝试来自 git 的最新开发中代码!最新版本通常缺少最新的错误修复、可用性改进和功能。参与行动可能会很有吸引力!

如果您是 Bevy 新手,这可能不适合您。您可能会更习惯使用已发布的版本。它将与社区插件和文档具有最佳的兼容性。

Bevy 的开发版本经常发生重大变化。因此,在实际项目中使用它可能会非常烦人。此外,第 3 方插件作者通常不屑于保持兼容性。您会经常遇到损坏,并且可能必须自己修复。

仅建议对更多实验或玩具项目执行此操作。

不过,有一些方法可以控制破损并使其不再成为问题。借助 Cargo,只要您准备好处理任何可能的重大更改,您就可以在方便时更新 Bevy。

您可能需要考虑分叉 Bevy 的存储库以及您使用的任何插件。使用您自己的分支可以让您根据需要轻松应用修复程序,或者编辑它们Cargo.toml以进行任何特殊配置以使您的项目正常运行。

如果您选择使用 Bevy main,我们强烈建议您在DiscordGitHub上与 Bevy 社区进行互动,这样您就可以跟踪正在发生的事情、获得帮助或参与讨论。

常见陷阱:神秘的编译错误

当在 Bevy 的不同版本之间进行更改时(例如,将现有项目从发布版本转换到 git 版本),您可能会遇到许多奇怪的意外构建错误。

Cargo.lock您通常可以通过删除目录来修复它们target

rm -rf Cargo.lock target

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

如果仍然出现错误,可能是因为 Cargo 正在尝试同时在依赖树中使用多个不同版本的 bevy。如果您使用的某些插件指定了与您的项目不同的 Bevy 版本/提交,则可能会发生这种情况。

如果您正在使用任何第三方插件,请考虑分叉它们,以便您可以编辑它们Cargo.toml并控制所有内容的配置方式。

Cargo Patches补丁

在某些情况下,您也许可以使用“Cargo Patches”来本地覆盖依赖项。例如,您可以通过执行以下操作来指定插件使用您的 fork of bevy,而无需分叉和编辑插件的 Cargo.toml

# replace the bevy git URL source with ours
# 将bevy git的URL源替换为我们的
[patch."https://github.com/bevyengine/bevy"]
# if we have our own fork
# 如果我们有自己的fork
bevy = { git = "https://github.com/me/bevy" }
# if we want to use a local path
# 如果我们想使用本地路径
bevy = { path = "../bevy" }
# some plugins might depend on individual bevy crates,
# 一些插件可能依赖于单个的bevy crate,
# instead of all of bevy, which means we need to patch
# 而不是所有的bevy,这意味着我们需要patch
# every individual bevy crate specifically:
# 每个独立的bevy crate,特别是:
bevy_ecs = { path = "../bevy/crates/bevy_ecs" }
bevy_app = { path = "../bevy/crates/bevy_app" }
# ...

# replace released versions of crates (crates.io source) with ours
# 用我们的替换已发布版本的crates(crates.io来源)
[patch.crates-io]
bevy_some_plugin = { git = "https://github.com/me/bevy_some_plugin", branch = "bevy_main" }
# also replace bevy itself
# 也替换bevy本身
bevy = { path = "../bevy" }
# ...

更新Bevy

建议您在 Cargo.toml中指定一个已知良好的 Bevy 提交 ,这样您就可以确保只在您真正想要更新时才更新它,从而避免不必要的损坏。

bevy = { git = "https://github.com/bevyengine/bevy", rev = "7a1bd34e" }

当您更改任何内容时,请务必运行:

cargo update

(或删除Cargo.lock

否则,您将面临因cargo无法正确解决依赖关系而出现错误的风险。

给插件作者的建议

如果您要发布插件crate,这里有一些建议:

  • 使用存储库中的主分支来定位 Bevy 的已发布版本
  • 在您的存储库中有一个单独的分支,以使对 bevy main 的支持与您的 bevy 发布版本的版本分开
  • 将信息放入自述文件中,告诉人们如何找到它
  • 设置 CI,以便在您的插件因一大堆新更改而损坏时通知您

请随意遵循此页面的所有建议,包括根据需要进行Cargo patches补丁。Cargo patches补丁仅在您直接构建项目时应用,而不是作为依赖项,因此它们不会影响您的用户,并且可以安全地保存在您的Cargo.toml.

CI 设置

以下是 GitHub Actions 的示例。这将在每天上午 8:00 (UTC) 运行,以验证您的代码是否仍然可以编译。失败时 GitHub 会通知您。

name: check if code still compiles

on:
  schedule:
    - cron: '0 8 * * *'

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Install Dependencies
        run: sudo apt-get update && sudo apt-get install g++ pkg-config libx11-dev libasound2-dev libudev-dev
      - uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          override: true

      - name: Check code
        run: cargo update && cargo check --lib --examples


(本节结束,见下一节4常见陷阱)

  • 20
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值