bevy初体验2-官方示例学习

官方示例学习

简介

学习一下 Bevy 官方示例代码,了解 Bevy 的使用方法和最佳实践。先从简单的示例(Bevy Desk Toy)开始,然后逐步深入到更复杂的示例。

目标

  • 熟悉 Bevy 的基本概念和架构
  • 掌握 Bevy 的核心功能和常用组件
  • 理解 Bevy 的事件系统和资源管理
  • 能够独立编写简单的 Bevy 应用程序

具体步骤

新建项目

使用 Cargo 创建一个新的 Bevy 项目:

bash 复制代码
   cargo new bevy-learning-game2
   cd bevy-learning-game2

添加依赖

bash 复制代码
   cargo add bevy

编写代码

在这里,直接使用 Bevy 官方示例代码作为学习材料。从官方仓库粘取代码:

rust 复制代码
//! Bevy logo as a desk toy using transparent windows! Now with Googly Eyes!
//!
//! This example demonstrates:
//! - Transparent windows that can be clicked through.
//! - Drag-and-drop operations in 2D.
//! - Using entity hierarchy, Transform, and Visibility to create simple animations.
//! - Creating simple 2D meshes based on shape primitives.

use bevy::{
    app::AppExit,
    input::common_conditions::{input_just_pressed, input_just_released},
    prelude::*,
    window::{CursorOptions, PrimaryWindow, WindowLevel},
};

#[cfg(target_os = "macos")]
use bevy::window::CompositeAlphaMode;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "Bevy Desk Toy".into(),
                transparent: true,
                #[cfg(target_os = "macos")]
                composite_alpha_mode: CompositeAlphaMode::PostMultiplied,
                ..default()
            }),
            ..default()
        }))
        .insert_resource(ClearColor(WINDOW_CLEAR_COLOR))
        .insert_resource(WindowTransparency(false))
        .insert_resource(CursorWorldPos(None))
        .add_systems(Startup, setup)
        .add_systems(
            Update,
            (
                get_cursor_world_pos,
                update_cursor_hit_test,
                (
                    start_drag.run_if(input_just_pressed(MouseButton::Left)),
                    end_drag.run_if(input_just_released(MouseButton::Left)),
                    drag.run_if(resource_exists::<DragOperation>),
                    quit.run_if(input_just_pressed(MouseButton::Right)),
                    toggle_transparency.run_if(input_just_pressed(KeyCode::Space)),
                    move_pupils.after(drag),
                ),
            )
                .chain(),
        )
        .run();
}

/// Whether the window is transparent
#[derive(Resource)]
struct WindowTransparency(bool);

/// The projected 2D world coordinates of the cursor (if it's within primary window bounds).
#[derive(Resource)]
struct CursorWorldPos(Option<Vec2>);

/// The current drag operation including the offset with which we grabbed the Bevy logo.
#[derive(Resource)]
struct DragOperation(Vec2);

/// Marker component for the instructions text entity.
#[derive(Component)]
struct InstructionsText;

/// Marker component for the Bevy logo entity.
#[derive(Component)]
struct BevyLogo;

/// Component for the moving pupil entity (the moving part of the googly eye).
#[derive(Component)]
struct Pupil {
    /// Radius of the eye containing the pupil.
    eye_radius: f32,
    /// Radius of the pupil.
    pupil_radius: f32,
    /// Current velocity of the pupil.
    velocity: Vec2,
}

// Dimensions are based on: assets/branding/icon.png
// Bevy logo radius
const BEVY_LOGO_RADIUS: f32 = 128.0;
// Birds' eyes x y (offset from the origin) and radius
// These values are manually determined from the logo image
const BIRDS_EYES: [(f32, f32, f32); 3] = [
    (145.0 - 128.0, -(56.0 - 128.0), 12.0),
    (198.0 - 128.0, -(87.0 - 128.0), 10.0),
    (222.0 - 128.0, -(140.0 - 128.0), 8.0),
];

const WINDOW_CLEAR_COLOR: Color = Color::srgb(0.2, 0.2, 0.2);

/// Spawn the scene
fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    // Spawn a 2D camera
    commands.spawn(Camera2d);

    // Spawn the text instructions
    let font = asset_server.load("fonts/FiraSans-Bold.ttf");
    let text_style = TextFont {
        font: font.clone(),
        font_size: 25.0,
        ..default()
    };
    commands.spawn((
        Text2d::new("Press Space to play on your desktop! Press it again to return.\nRight click Bevy logo to exit."),
            text_style.clone(),
            Transform::from_xyz(0.0, -300.0, 100.0),
        InstructionsText,
    ));

    // Create a circle mesh. We will reuse this mesh for all our circles.
    let circle = meshes.add(Circle { radius: 1.0 });
    // Create the different materials we will use for each part of the eyes. For this demo they are basic [`ColorMaterial`]s.
    let outline_material = materials.add(Color::BLACK);
    let sclera_material = materials.add(Color::WHITE);
    let pupil_material = materials.add(Color::srgb(0.2, 0.2, 0.2));
    let pupil_highlight_material = materials.add(Color::srgba(1.0, 1.0, 1.0, 0.2));

    // Spawn the Bevy logo sprite
    commands
        .spawn((
            Sprite::from_image(asset_server.load("branding/icon.png")),
            BevyLogo,
        ))
        .with_children(|commands| {
            // For each bird eye
            for (x, y, radius) in BIRDS_EYES {
                let pupil_radius = radius * 0.6;
                let pupil_highlight_radius = radius * 0.3;
                let pupil_highlight_offset = radius * 0.3;
                // eye outline
                commands.spawn((
                    Mesh2d(circle.clone()),
                    MeshMaterial2d(outline_material.clone()),
                    Transform::from_xyz(x, y - 1.0, 1.0)
                        .with_scale(Vec2::splat(radius + 2.0).extend(1.0)),
                ));

                // sclera
                commands.spawn((
                    Transform::from_xyz(x, y, 2.0),
                    Visibility::default(),
                    children![
                        // sclera
                        (
                            Mesh2d(circle.clone()),
                            MeshMaterial2d(sclera_material.clone()),
                            Transform::from_scale(Vec3::new(radius, radius, 0.0)),
                        ),
                        // pupil
                        (
                            Transform::from_xyz(0.0, 0.0, 1.0),
                            Visibility::default(),
                            Pupil {
                                eye_radius: radius,
                                pupil_radius,
                                velocity: Vec2::ZERO,
                            },
                            children![
                                // pupil main
                                (
                                    Mesh2d(circle.clone()),
                                    MeshMaterial2d(pupil_material.clone()),
                                    Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::new(
                                        pupil_radius,
                                        pupil_radius,
                                        1.0,
                                    )),
                                ),
                                // pupil highlight
                                (
                                    Mesh2d(circle.clone()),
                                    MeshMaterial2d(pupil_highlight_material.clone()),
                                    Transform::from_xyz(
                                        -pupil_highlight_offset,
                                        pupil_highlight_offset,
                                        1.0,
                                    )
                                    .with_scale(Vec3::new(
                                        pupil_highlight_radius,
                                        pupil_highlight_radius,
                                        1.0,
                                    )),
                                )
                            ],
                        )
                    ],
                ));
            }
        });
}

/// Project the cursor into the world coordinates and store it in a resource for easy use
fn get_cursor_world_pos(
    mut cursor_world_pos: ResMut<CursorWorldPos>,
    primary_window: Single<&Window, With<PrimaryWindow>>,
    q_camera: Single<(&Camera, &GlobalTransform)>,
) {
    let (main_camera, main_camera_transform) = *q_camera;
    // Get the cursor position in the world
    cursor_world_pos.0 = primary_window.cursor_position().and_then(|cursor_pos| {
        main_camera
            .viewport_to_world_2d(main_camera_transform, cursor_pos)
            .ok()
    });
}

/// Update whether the window is clickable or not
fn update_cursor_hit_test(
    cursor_world_pos: Res<CursorWorldPos>,
    primary_window: Single<(&Window, &mut CursorOptions), With<PrimaryWindow>>,
    bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
) {
    let (window, mut cursor_options) = primary_window.into_inner();
    // If the window has decorations (e.g. a border) then it should be clickable
    if window.decorations {
        cursor_options.hit_test = true;
        return;
    }

    // If the cursor is not within the window we don't need to update whether the window is clickable or not
    let Some(cursor_world_pos) = cursor_world_pos.0 else {
        return;
    };

    // If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable
    cursor_options.hit_test = bevy_logo_transform
        .translation
        .truncate()
        .distance(cursor_world_pos)
        < BEVY_LOGO_RADIUS;
}

/// Start the drag operation and record the offset we started dragging from
fn start_drag(
    mut commands: Commands,
    cursor_world_pos: Res<CursorWorldPos>,
    bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
) {
    // If the cursor is not within the primary window skip this system
    let Some(cursor_world_pos) = cursor_world_pos.0 else {
        return;
    };

    // Get the offset from the cursor to the Bevy logo sprite
    let drag_offset = bevy_logo_transform.translation.truncate() - cursor_world_pos;

    // If the cursor is within the Bevy logo radius start the drag operation and remember the offset of the cursor from the origin
    if drag_offset.length() < BEVY_LOGO_RADIUS {
        commands.insert_resource(DragOperation(drag_offset));
    }
}

/// Stop the current drag operation
fn end_drag(mut commands: Commands) {
    commands.remove_resource::<DragOperation>();
}

/// Drag the Bevy logo
fn drag(
    drag_offset: Res<DragOperation>,
    cursor_world_pos: Res<CursorWorldPos>,
    time: Res<Time>,
    mut bevy_transform: Single<&mut Transform, With<BevyLogo>>,
    mut q_pupils: Query<&mut Pupil>,
) {
    // If the cursor is not within the primary window skip this system
    let Some(cursor_world_pos) = cursor_world_pos.0 else {
        return;
    };

    // Calculate the new translation of the Bevy logo based on cursor and drag offset
    let new_translation = cursor_world_pos + drag_offset.0;

    // Calculate how fast we are dragging the Bevy logo (unit/second)
    let drag_velocity =
        (new_translation - bevy_transform.translation.truncate()) / time.delta_secs();

    // Update the translation of Bevy logo transform to new translation
    bevy_transform.translation = new_translation.extend(bevy_transform.translation.z);

    // Add the cursor drag velocity in the opposite direction to each pupil.
    // Remember pupils are using local coordinates to move. So when the Bevy logo moves right they need to move left to
    // simulate inertia, otherwise they will move fixed to the parent.
    for mut pupil in &mut q_pupils {
        pupil.velocity -= drag_velocity;
    }
}

/// Quit when the user right clicks the Bevy logo
fn quit(
    cursor_world_pos: Res<CursorWorldPos>,
    mut app_exit: MessageWriter<AppExit>,
    bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
) {
    // If the cursor is not within the primary window skip this system
    let Some(cursor_world_pos) = cursor_world_pos.0 else {
        return;
    };

    // If the cursor is within the Bevy logo radius send the [`AppExit`] event to quit the app
    if bevy_logo_transform
        .translation
        .truncate()
        .distance(cursor_world_pos)
        < BEVY_LOGO_RADIUS
    {
        app_exit.write(AppExit::Success);
    }
}

/// Enable transparency for the window and make it on top
fn toggle_transparency(
    mut commands: Commands,
    mut window_transparency: ResMut<WindowTransparency>,
    mut q_instructions_text: Query<&mut Visibility, With<InstructionsText>>,
    mut primary_window: Single<&mut Window, With<PrimaryWindow>>,
) {
    // Toggle the window transparency resource
    window_transparency.0 = !window_transparency.0;

    // Show or hide the instructions text
    for mut visibility in &mut q_instructions_text {
        *visibility = if window_transparency.0 {
            Visibility::Hidden
        } else {
            Visibility::Visible
        };
    }

    // Remove the primary window's decorations (e.g. borders), make it always on top of other desktop windows, and set the clear color to transparent
    // only if window transparency is enabled
    let clear_color;
    (
        primary_window.decorations,
        primary_window.window_level,
        clear_color,
    ) = if window_transparency.0 {
        (false, WindowLevel::AlwaysOnTop, Color::NONE)
    } else {
        (true, WindowLevel::Normal, WINDOW_CLEAR_COLOR)
    };

    // Set the clear color
    commands.insert_resource(ClearColor(clear_color));
}

/// Move the pupils and bounce them around
fn move_pupils(time: Res<Time>, mut q_pupils: Query<(&mut Pupil, &mut Transform)>) {
    for (mut pupil, mut transform) in &mut q_pupils {
        // The wiggle radius is how much the pupil can move within the eye
        let wiggle_radius = pupil.eye_radius - pupil.pupil_radius;
        // Store the Z component
        let z = transform.translation.z;
        // Truncate the Z component to make the calculations be on [`Vec2`]
        let mut translation = transform.translation.truncate();
        // Decay the pupil velocity
        pupil.velocity *= ops::powf(0.04f32, time.delta_secs());
        // Move the pupil
        translation += pupil.velocity * time.delta_secs();
        // If the pupil hit the outside border of the eye, limit the translation to be within the wiggle radius and invert the velocity.
        // This is not physically accurate but it's good enough for the googly eyes effect.
        if translation.length() > wiggle_radius {
            translation = translation.normalize() * wiggle_radius;
            // Invert and decrease the velocity of the pupil when it bounces
            pupil.velocity *= -0.75;
        }
        // Update the entity transform with the new translation after reading the Z component
        transform.translation = translation.extend(z);
    }
}

然后构建一下项目:

bash 复制代码
   cargo build

添加资源

在项目根目录创建一个assets文件夹,并将图片和字体文件放入指定位置:

css 复制代码
bevy-learning-game2/
├── assets/
│   ├── branding/icon.png
│   └── fonts/FiraSans-Bold.ttf
├── Cargo.toml
└── src/main.rs

icon.png FiraSans-Bold.ttf

效果

运行项目查看效果:

bash 复制代码
   cargo run

可以看到窗口菜单弹出,显示 Bevy logo 和说明文字。按空格键开始或回到菜单页,左键拖动,图片小鸟的眼珠在晃动,右键点击退出程序。

思路梳理

main函数解析

先从main函数开始,添加DefaultPlugins插件组,设置窗口透明属性,然后添加各种资源和系统。系统分为启动时运行的setup系统和每帧更新的系统。 这里面有个细节,就是在设置窗口透明属性时,使用了条件编译#[cfg(target_os = "macos")],这是因为在macOS上需要设置特定的composite_alpha_mode属性以支持透明窗口。 为什么要这么操作,点击进transparent属性的定义可以看到:

rust 复制代码
    /// Should the window be transparent?
    ///
    /// Defines whether the background of the window should be transparent.
    ///
    /// ## Platform-specific
    /// - iOS / Android / Web: Unsupported.
    /// - macOS: Not working as expected.
    ///
    /// macOS transparent works with winit out of the box, so this issue might be related to: <https://github.com/gfx-rs/wgpu/issues/687>.
    /// You should also set the window `composite_alpha_mode` to `CompositeAlphaMode::PostMultiplied`.
    pub transparent: bool,

可以看到在macOS上透明窗口需要额外设置composite_alpha_mode属性为CompositeAlphaMode::PostMultiplied,否则当程序启动后,按下空格键开始透明窗口时,背景会变成全黑,达不到预期效果。

Startup

Startup意味着setup函数只会在启动时执行一次,接下来看setup函数,主要是创建2D相机,添加说明文字,创建圆形网格和材质,然后生成Bevy logo实体和眼睛实体。这里使用了Bevy的层级系统,将眼睛作为Bevy logo的子实体,这样当拖动Bevy logo时,眼睛会跟着一起移动。

当加入文字的时候,我们可以看到,使用了FiraSans-Bold.ttf字体文件,这个文件放在assets/fonts目录下,但是load函数中使用的路径是"fonts/FiraSans-Bold.ttf"。

rust 复制代码
let font = asset_server.load("fonts/FiraSans-Bold.ttf");

这是因为Bevy默认会将assets目录作为资源根目录,所以在代码中引用资源时,只需要相对于assets目录的路径即可。我们可以从AssetPlugin的定义中看到这一点:

rust 复制代码
pub struct AssetPlugin {
    /// The default file path to use (relative to the project root) for unprocessed assets.
    pub file_path: String,
    /// The default file path to use (relative to the project root) for processed assets.
    pub processed_file_path: String,
    /// If set, will override the default "watch for changes" setting. By default "watch for changes" will be `false` unless
    /// the `watch` cargo feature is set. `watch` can be enabled manually, or it will be automatically enabled if a specific watcher
    /// like `file_watcher` is enabled.
    ///
    /// Most use cases should leave this set to [`None`] and enable a specific watcher feature such as `file_watcher` to enable
    /// watching for dev-scenarios.
    pub watch_for_changes_override: Option<bool>,
    /// The [`AssetMode`] to use for this server.
    pub mode: AssetMode,
    /// How/If asset meta files should be checked.
    pub meta_check: AssetMetaCheck,
    /// How to handle load requests of files that are outside the approved directories.
    ///
    /// Approved folders are [`AssetPlugin::file_path`] and the folder of each
    /// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid.
    pub unapproved_path_mode: UnapprovedPathMode,
}

可以看到file_path是定义资源文件对于项目根目录的相对目录的,然后看到Default实现:

rust 复制代码
impl Default for AssetPlugin {
    fn default() -> Self {
        Self {
            mode: AssetMode::Unprocessed,
            file_path: Self::DEFAULT_UNPROCESSED_FILE_PATH.to_string(),
            processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(),
            watch_for_changes_override: None,
            meta_check: AssetMetaCheck::default(),
            unapproved_path_mode: UnapprovedPathMode::default(),
        }
    }
}

可以看到file_path的默认值是常量DEFAULT_UNPROCESSED_FILE_PATH,然后就可以看到这个常量的定义:

rust 复制代码
impl AssetPlugin {
    const DEFAULT_UNPROCESSED_FILE_PATH: &'static str = "assets";
    /// NOTE: this is in the Default sub-folder to make this forward compatible with "import profiles"
    /// and to allow us to put the "processor transaction log" at `imported_assets/log`
    const DEFAULT_PROCESSED_FILE_PATH: &'static str = "imported_assets/Default";
}

可以看到默认的资源目录就是assets,所以在代码中引用资源时,只需要相对于assets目录的路径即可。

在将说明文字实体添加进world中时,需要设置位置信息,在2d画面中z轴表示前后顺序,z值越大,实体显示在越前面。这里将说明文字的z值设置为100.0,确保它显示在最前面,不会被Bevy logo遮挡。类似于html中的z-index属性。

rust 复制代码
    commands.spawn((
        Text2d::new("Press Space to play on your desktop! Press it again to return.\nRight click Bevy logo to exit."),
            text_style.clone(),
            Transform::from_xyz(0.0, -300.0, 100.0),
        InstructionsText,
    ));

在绘制眼睛的子实体时,使用了两种不同的方法,一种是直接调用commands.spawn().with_children(),另一种是使用了一个宏children!,这个宏用于简化子实体的创建。它允许我们在一个闭包中定义多个子实体,而不需要重复调用commands.spawn()。这样代码更加简洁易读。

眼睛的组成部分包括眼睛轮廓、眼白、瞳孔和高光,每个部分都是一个独立的实体,并且都有自己的网格、材质和变换信息。父子级关系如下

css 复制代码
Bevy Logo
 ├── Eye 1
 │    ├── Outline
 │    ├── Sclera
 │    └── Pupil
 │         ├── Pupil Main
 │         └── Pupil Highlight
 ├── Eye 2
 │    ├── Outline
 │    ├── Sclera
 │    └── Pupil
 │         ├── Pupil Main
 │         └── Pupil Highlight
 └── Eye 3
      ├── Outline
      ├── Sclera
      └── Pupil
           ├── Pupil Main
           └── Pupil Highlight

函数中眼睛的图像组成的实现就是按照这个层级结构来实现的。

Update

Update意味着这些系统会在每一帧更新时执行。这里的系统包括获取光标位置、更新光标命中测试、拖动操作、退出程序、切换透明度和移动瞳孔。

此处是通过.chain()方法将两个系统和一个组合的系统组合在一起次序执行。这样可以确保在每一帧更新时,先获取光标位置,然后更新光标命中测试,再处理拖动操作、退出程序、切换透明度和移动瞳孔。

get_cursor_world_pos

获取光标在世界坐标系中的位置,并存储在CursorWorldPos资源中。它使用了相机的viewport_to_world_2d方法将屏幕坐标转换为世界坐标。 理解这个函数的意义,需要先了解世界坐标和屏幕坐标的区别。世界坐标是游戏中的实际位置,而屏幕坐标是用户界面上的位置(即相机拍出的画面的位置)。通过将屏幕坐标转换为世界坐标,我们可以在游戏中正确地处理光标位置。

update_cursor_hit_test

根据光标位置更新窗口的可点击状态。如果光标在Bevy logo范围内,窗口是可点击的,否则不可点击。 这个系统的作用是为了实现透明窗口的点击穿透效果。当窗口透明时,用户可以点击窗口下方的其他应用程序。通过判断光标是否在Bevy logo范围内,我们可以决定窗口是否应该响应点击事件。

functions with run_if

在Bevy中,run_if用于条件性地运行系统。它允许我们根据特定条件决定是否执行某个系统。 在这个示例中,run_if被用来控制拖动操作、退出程序和切换透明度的系统。例如,

rust 复制代码
start_drag.run_if(input_just_pressed(MouseButton::Left)),

这表示只有在左键刚刚按下时,才会执行start_drag系统。类似地,

rust 复制代码
end_drag.run_if(input_just_released(MouseButton::Left)),

表示只有在左键刚刚释放时,才会执行end_drag系统。

这里有一个小细节,start_drag和end_drag是函数,为什么能直接调用run_if方法呢?我们直接看代码:

首先看run_if的定义:

rust 复制代码
#[diagnostic::on_unimplemented(
    message = "`{Self}` does not describe a valid system configuration",
    label = "invalid system configuration"
)]
pub trait IntoScheduleConfigs<T: Schedulable<Metadata = GraphInfo, GroupMetadata = Chain>, Marker>:
    Sized
{
    // 其余方法省略
    fn run_if<M>(self, condition: impl SystemCondition<M>) -> ScheduleConfigs<T> {
        self.into_configs().run_if(condition)
    }
}

可以看到run_if是定义在IntoScheduleConfigs特征(Trait)中的,这就要求start_drag和end_drag这类的函数必须实现了IntoScheduleConfigs特征(Trait)。

再看add_systems的定义:

rust 复制代码
    pub fn add_systems<M>(
        &mut self,
        schedule: impl ScheduleLabel,
        systems: impl IntoScheduleConfigs<ScheduleSystem, M>,
    ) -> &mut Self {
        self.main_mut().add_systems(schedule, systems);
        self
    }

可以看到add_systems的第二个参数是一个实现了IntoScheduleConfigs特征(Trait)的类型,这也就意味着start_drag和end_drag这类的函数必须实现了IntoScheduleConfigs特征(Trait)。因为如果没有调用run_if方法的话,add_systems的第二个参数就是一个函数,这里函数类型实现了IntoScheduleConfigs特征(Trait),所以可以直接传递给add_systems方法。

而这也意味着,run_if的返回值类型也是实现了IntoScheduleConfigs特征(Trait)的类型,这样就可以继续传递给add_systems方法。 我们来看run_if的返回值类型为ScheduleConfigs,这也就一位置ScheduleConfigs也实现了IntoScheduleConfigs特征(Trait)。然后我们通过self.into_configs().run_if(condition)的run_if进入到方法定义中:

rust 复制代码
impl<T: Schedulable<Metadata = GraphInfo, GroupMetadata = Chain>> IntoScheduleConfigs<T, ()>
    for ScheduleConfigs<T>
{
    fn run_if<M>(mut self, condition: impl SystemCondition<M>) -> ScheduleConfigs<T> {
        self.run_if_dyn(new_condition(condition));
        self
    }
}

可以看到ScheduleConfigs也实现了IntoScheduleConfigs特征(Trait),所以可以继续传递给add_systems方法。

至此,我们仍然没能解释清楚为什么函数类型能实现IntoScheduleConfigs特征(Trait),接着看在上述代码下方的IntoScheduleConfigs特征(Trait)的另一个实现:

rust 复制代码
impl<F, Marker> IntoScheduleConfigs<ScheduleSystem, Marker> for F
where
    F: IntoSystem<(), (), Marker>,
{
    fn into_configs(self) -> ScheduleConfigs<ScheduleSystem> {
        let boxed_system = Box::new(IntoSystem::into_system(self));
        ScheduleConfigs::ScheduleConfig(ScheduleSystem::into_config(boxed_system))
    }
}

可以看到IntoSystem特征(Trait)也实现了IntoScheduleConfigs特征(Trait),然后看IntoSystem特征(Trait)的定义:

rust 复制代码
/// Conversion trait to turn something into a [`System`].
///
/// Use this to get a system from a function. Also note that every system implements this trait as
/// well.
///
/// # Usage notes
///
/// This trait should only be used as a bound for trait implementations or as an
/// argument to a function. If a system needs to be returned from a function or
/// stored somewhere, use [`System`] instead of this trait.
///
/// # Examples
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// fn my_system_function(a_usize_local: Local<usize>) {}
///
/// let system = IntoSystem::into_system(my_system_function);
/// ```
// This trait has to be generic because we have potentially overlapping impls, in particular
// because Rust thinks a type could impl multiple different `FnMut` combinations
// even though none can currently
#[diagnostic::on_unimplemented(
    message = "`{Self}` is not a valid system with input `{In}` and output `{Out}`",
    label = "invalid system"
)]
pub trait IntoSystem<In: SystemInput, Out, Marker>: Sized {
    /// 忽略具体实现细节
}

从注释就可以看出,IntoSystem特征(Trait)通过IntoSystem::into_system(my_system_function)的形式将函数转换为系统(System)。 然后在上述代码下方可以看到IntoSystem特征(Trait)的一个实现:

rust 复制代码
// All systems implicitly implement IntoSystem.
impl<T: System> IntoSystem<T::In, T::Out, ()> for T {
    type System = T;
    fn into_system(this: Self) -> Self {
        this
    }
}

可以看到所有实现了System特征(Trait)的类型都隐式地实现了IntoSystem特征(Trait),而函数类型实现了System特征(Trait),所以函数类型也就实现了IntoSystem特征(Trait)。从而实现了IntoScheduleConfigs特征(Trait)。 因此,函数类型可以通过实现IntoSystem特征(Trait)来实现IntoScheduleConfigs特征(Trait),从而能够调用run_if方法。

事情到这里,似乎已经水落石出了,但是还有一个小问题,就是函数类型是如何实现System特征(Trait)的呢? 因为我们的代码并没有显式的使用IntoSystem::into_system(my_system_function)来将函数转换为系统(System)。

我们继续看IntoSystem特征(Trait)的实现,可以找到一个叫做SystemParamFunction的Trait对IntoSystem的实现:

rust 复制代码
impl<Marker, Out, F> IntoSystem<F::In, Out, (IsFunctionSystem, Marker)> for F
where
    Out: 'static,
    Marker: 'static,
    F: SystemParamFunction<Marker, Out: IntoResult<Out>>,
{
    type System = FunctionSystem<Marker, Out, F>;
    fn into_system(func: Self) -> Self::System {
        FunctionSystem {
            func,
            #[cfg(feature = "hotpatching")]
            current_ptr: subsecond::HotFn::current(<F as SystemParamFunction<Marker>>::run)
                .ptr_address(),
            state: None,
            system_meta: SystemMeta::new::<F>(),
            marker: PhantomData,
        }
    }
}

再继续往下看,可以找到一个叫做impl_system_function的宏:

rust 复制代码
macro_rules! impl_system_function {
    // 忽略具体实现细节
}

根据宏的定义,可以看出这段宏 impl_system_function! 为任意数量的系统参数生成两组 SystemParamFunction 的实现,让函数或闭包可以被 ECS 作为系统执行。

第一组实现面向无输入的系统函数 fn($param...) -> Out。约束中要求 Func 同时实现对原始参数类型和对应的 SystemParamItem 的 FnMut,以便既能直接用系统参数类型调用,也能用解析后的参数项调用。run 解构系统参数元组,并通过内部的 call_inner 调用 self,这是为绕过 rustc 在多重 FnMut 实现下的识别问题。

第二组实现面向具有显式系统输入 In 的函数 fn(In, $param...) -> Out。In 必须实现 SystemInput,run 接收底层输入类型 In::Inner<'_>,再用 In::wrap 转换为 In::Param<'_>,随后同样通过 call_inner 调用 self。PhantomData 仅用于在泛型中保持类型关联而不占用存储。

整体作用是自动为不同参数组合和可选系统输入生成 SystemParamFunction 实现,减少样板代码,让系统函数在调度时可被正确调用。

然后下面就是这个宏的调用:

rust 复制代码
// Note that we rely on the highest impl to be <= the highest order of the tuple impls
// of `SystemParam` created.
all_tuples!(impl_system_function, 0, 16, F);

根据这个宏的定义,可以批量为不同数量的参数生成SystemParamFunction的实现,从而让函数类型能够作为系统(System)被调度执行。

至此,终于弄清楚了函数类型是如何实现System特征(Trait)的,从而能够调用run_if方法。

  1. 通过all_tuples宏调用impl_system_function宏,为不同数量的参数的函数生成SystemParamFunction的实现。
  2. SystemParamFunction的实现了IntoSystem特征(Trait)。所以也可以通过IntoSystem::into_system将函数转换为系统(System)。
  3. IntoSystem特征(Trait)实现了IntoScheduleConfigs特征(Trait)。

经过以上步骤,函数类型最终实现了IntoScheduleConfigs特征(Trait),从而能够调用run_if方法。

start_drag

如果光标在 Bevy 标志的半径范围内,开始拖拽操作,并记住光标相对于原点的偏移量 具体到代码实现,则是将光标位置与 Bevy 标志位置的偏移量存储在 DragOperation 资源中,以便后续拖拽时使用。

end_drag

结束当前的拖拽操作,移除 DragOperation 资源。

drag

根据光标位置和拖拽偏移量更新 Bevy 标志的位置。同时,根据拖拽速度更新瞳孔的速度,实现瞳孔的惯性效果。

quit

如果光标在 Bevy 标志范围内,发送 AppExit 事件退出应用程序

toggle_transparency

切换窗口的透明状态。如果启用透明状态,隐藏说明文字,移除窗口装饰,并将窗口置于所有桌面窗口之上。同时更新清除颜色为透明或默认颜色。

move_pupils

根据瞳孔的速度移动瞳孔,并在瞳孔碰到眼睛边缘时反弹。通过衰减速度实现瞳孔的减速效果。

总结

通过学习 Bevy 官方示例代码,了解了 Bevy 的基本概念和架构,掌握了核心功能和常用组件,理解了事件系统和资源管理。

相关推荐
alwaysrun1 天前
Rust 如何实现许可证管理系统
rust
编码浪子1 天前
《安全 Rust 的边界在哪?》— 中文解读
开发语言·安全·rust
不知名的老吴1 天前
聊一聊年轻的编程语言Golang与Rust
开发语言·golang·rust
开开心心就好1 天前
支持批量处理的视频分割工具推荐
安全·智能手机·rust·pdf·电脑·1024程序员节·lavarel
浪客川1 天前
UniFFI 跨平台开发Rust 与 Android (Kotlin) 集成
android·rust·kotlin
芝士就是力量啊 ೄ೨1 天前
如何配置Rust、Git,并从Github上拉下一个项目
git·rust·github
eqwaak01 天前
4 月技术快讯|Rust 1.90 正式发布,系统级开发再进化
开发语言·后端·rust
techdashen2 天前
Cloudflare 如何把一个大型代理拆成三个小服务来提升可靠性
开发语言·rust