官方示例学习
简介
学习一下 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
效果
运行项目查看效果:
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方法。
- 通过all_tuples宏调用impl_system_function宏,为不同数量的参数的函数生成SystemParamFunction的实现。
- SystemParamFunction的实现了IntoSystem特征(Trait)。所以也可以通过IntoSystem::into_system将函数转换为系统(System)。
- 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 的基本概念和架构,掌握了核心功能和常用组件,理解了事件系统和资源管理。