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 的基本概念和架构,掌握了核心功能和常用组件,理解了事件系统和资源管理。

相关推荐
葡萄城技术团队5 小时前
Hurley:用 Rust 打造的高性能 HTTP 客户端 + 压测工具
开发语言·http·rust
Source.Liu8 小时前
【dxf-rs】库全面介绍
rust·dxf-rs
土豆125016 小时前
Rust宏编程完全指南:用元编程解锁Rust的终极力量
rust·编程语言
小杍随笔20 小时前
【Rust 语言编程知识与应用:基础数据类型详解】
开发语言·后端·rust
小杍随笔1 天前
【Rust 语言编程知识与应用:自定义数据类型详解】
开发语言·后端·rust
咚为1 天前
Rust 跨平台编译实战:从手动配置到 Cross 容器化
开发语言·后端·rust
幸福指北1 天前
我用 Tauri + Vue 3 + Rust 开发了一款跨平台网络连接监控工具Portview,性能炸裂!
前端·网络·vue.js·tcp/ip·rust
咚为1 天前
深入浅出 Rust FFI:从内存安全到二进制兼容
开发语言·安全·rust
a1117761 天前
剪切板助手TieZ(开源项目rust)
rust·开源·剪切板