README.md
Unofficial Bevy Cheat Book自用版翻译,学到哪翻到哪,随缘更新。完整翻译可以看:Bevy 游戏引擎开发指南。
License
Copyright © 2021-2022 Ida Iyes.
All code in the book is provided under the MIT-0 License. At your option, you may also use it under the regular MIT License.
The text of the book is provided under the CC BY-NC-SA 4.0.
Latest commit da61c29
文本编辑器 / 集成开发环境
todo
IntelliJ
使用 Query 时 IDE 的类型提示会失效,解决办法是在“实验性功能”中开启 Rust 的过程宏支持(issues #6908):
1,在 IDE “随处搜索”中搜索“实验性功能”;
2,启用 org.rust.macros.proc。
Latest commit bae8bf8
坐标系统
Bevy 使用右手坐标系。为了保持一致性,Bevy 在 2D、3D 和 UI 上使用相同的坐标系。
用 2D 来解释最简单:
- X 轴从左指向右(X 的正方向指向右)
- Y 轴从下指向上(Y 的正方向指向上)
- Z 轴从远处指向近处(Z 的正方向指向观察者)
- 原点(X=0.0,Y=0.0)默认在屏幕中心(2D)或者在左下角(UI)
在处理 2D 精灵时,可以把背景放在 Z=0.0,并把其他精灵放置在递增的正向 Z 坐标上,使它们层叠起来。
在 3D 中,轴的方向跟 2D 的一样。
Bevy 的坐标系和 Godot、Maya、OpenGL 的一样。与 Unity 相比,Z 轴是倒置的。

(graphic modifed and used with permission; original by @FreyaHolmer)
注意:在 Bevy 中,Y 轴总是指向上。
使用 UI(与网页相反)时,或者习惯使用 Y 轴指向下方的 2D 库时,这可能会感觉不直观。查看 [UI layout is inverted]。
在制作 2D 游戏时还要注意一个常见的陷阱:摄像机必须定位在一个较远的 Z 坐标(默认为 999.9),否则可能无法看到精灵。查看 [Cannot see sprites in 2D]。
Latest commit 789b9ee
变换
相关的官方文档:[transform]、[translation]、[rotation]、[3d_rotation]、[scale]、[move_sprite]、[parenting]
变换使得游戏物体能够被放置在游戏世界中。物体的“平移”(位置/坐标)、“旋转”和“比例”(大小调整)的组合就是变换:
- 平移:移动物体
- 旋转:旋转物体
- 比例:放大或缩小物体
#![allow(unused)] fn main() { // 平移 let xf_pos567 = Transform::from_xyz(5.0, 6.0, 7.0); // 缩放 let xf_scale = Transform::from_scale(Vec3::splat(2.0)); // 旋转 2d 物体(绕 Z 轴旋转) 30° let xf_rot2d = Transform::from_rotation(Quat::from_rotation_z(30.0_f32).to_radians()) // 3d 旋转 --- 略 // 变换 let xf = Transform::from_xyz(1.0, 2.0, 3.0) .with_scale(Vec3::new(0.5, 0.5, 1.0)) .with_rotation(Quat::from_rotation_y(0.125 * std::f32::conts::PI)); }
变换组件
在 Bevy 中,变换由两个组件表示:Transform 和 GlobalTransform。如果某个实体表示的是游戏世界中的物体,那么这个实体必须同时拥有 Transform 和 GlobalTransform。
所有 Bevy 内置的 Bundle 类型都包含 Transform 和 GlobalTransform。自定义 Bundle
类型可以添加这些 Bundle 来确保包含 Trasform 和 GlobalTransform:
- SpatialBundle:transform + visibility
- TransformBundle:transform
#![allow(unused)] fn main() { fn 生成特殊实体( mut commands: Commands, ) { // 创建一个实体,不使用内置包,但需要 transform 和 visibility 功能 commands.spawn() .insert(ComponentA) .insert(ComponentB) .insert_bundle(SpatialBundle { transform: Transform::from_scale(Vec3::splat(3.0)), visibility: Visibility { is_visible: false, }, }); } }
Transform 组件
Transform 最常用,是一个包含平移、旋转和缩放的结构体。要读取或操作这些值,请在系统中用 Query 来访问。
如果实体有父实体,Transform 组件是相对于父实体的。这意味着子实体会与父实体一起移动/旋转/缩放。
#![allow(unused)] fn main() { fn 给气球打气 ( mut query: Query<&mut Transform, With<Balloon>>, keyboard: Res<Input<KeyCode>>, ) { // 每次按下空格键,所有气球扩大 25% if keyboard.just_pressed(KeyCode::Space) { for mut transform in &mut query { transform.scale *= 1.25; } } } }
GlobalTransform 组件
GlobalTransform 表示世界中的绝对全局位置。如果实体没有父实体,那么这个值和 Transform 相同。GlobalTransform 的值是由 Bevy 内部计算/管理的,视为只读数据,不要进行可变修改。
变换传播
注意:Transform 和 GlobalTransform 的同步是由运行在 PostUpdate 阶段的内部系统(“变换传播系统”)管理的,如果手动修改 Transform,GlobalTransform 不会立即更新。也就是:在当前帧里更新 Transform 后立即获取 GlobalTransform 的数值,GlobalTransform 不变,要等到下一帧才能获取正确的 GlobalTransform。
如果要立即处理 GlobalTransform,必须把系统放在 PostUpdate 阶段,并用标签让系统跟在 TransformSystem::TransformPropagate 后面运行。
/// 打印“当前帧”的最新全局位置 fn 调试全局变换( query: Query<&GlobalTransform, With<Player>>, ) { let gxf = query.single(); debug!("玩家位于:{:?}", gxf.translation()); } fn main() { use bevy::transform::TransformSystem; App::new() .add_plugins(DefaultPlugins) .add_system_to_stage( CoreStage::PostUpdate, 调试全局变换.after(TransformSystem::TransformPropagete) ) .run(); }
Visibility
Latest commit 23576f7
时间和计时器
相关的官方文档:[timers]、[move_sprite]
时间
Time 是全局资源,可以从任何系统访问来获取时间信息。
Bevy 在每帧开始时更新这些值。
增量时间
最常用的是 delta 时间:上一帧和当前帧之间经过了多少时间。可以通过 delta 得知游戏运行的速度,使用 delta 能让游戏忽略帧率平稳地运行。
#![allow(unused)] fn main() { fn asteroids_fly( time: Res<Time>, mut q: Query<&mut Transform, With<Asteroid>>, ) { for mut transform in q.iter_mut() { // 沿 X 轴以每秒 10.0 单位的速度移动小行星 transform.translation.x += 10.0 * time.delta_seconds(); } } }
持续时间
Time 还提供自 App 启动以来的总运行时间。
#![allow(unused)] fn main() { use std::time::Instant; /// 目标时获取实体生成时的具体时间 #[derive(Component)] struct SpawnedTime(Instant); fn spawn_my_stuff( mut commands: Commands, time: Res<Time>, ) { commands .spawn() .insert(SpawnedTime(time.startup() + time.time_since_startup())); } }
计时器和秒表
Timer 和 Stopwatch 可以处理时间间隔和定时。可以在自定义的组件和资源中使用。
计时器和秒表需要系统来驱动:在系统中调用 .tick(delta) 函数来使计时器、秒表工作,否则不会开始计时。可以在 Time 资源查看详细信息。
计时器
Timer 可以检测流逝的时间。计时器有一个设定的时间。计时器可以设置成“重复”或“不重复”。
计时器可以手动“重置”(重新开始)和“暂停”(即使调用 .tick() 也不会工作)。
设定为“重复”的计时器会在达到设定的持续时间时自动重置。
todo:需要测试 Use .finished() to detect when a timer has reached its set duration. Use .just_finished(), if you need to detect only on the exact tick when the duration was reached.
#![allow(unused)] fn main() { use std::time::Duration; #[derive(Component)] struct FuseTime { /// 炸弹爆炸倒计时(不重复计时) timer: Timer, } fn 引爆炸弹( mut commands: Commands, mut q: Query<(Entity, &mut FuseTime)>, time: Res<Time>, ) { for (entity, mut fuse_timer) in q.iter_mut() { // 计时器必须“滴答”计时才能工作 fuse_timer.timer.tick(time.delta()); // 计时完成,由于世界和平,炸弹会被拆除 if fuse_timer.timer.finished() { commands.entity(entity).despawn(); } } } struct BombsSpawnConfig { // 定义多久产生新的炸弹(重复计时) timer: Timer, } /// 隔一段时间产生新的炸弹 fn 生成炸弹( mut commands: Commands, time: Res<Time>, mut config: ResMut<BombsSpawnConfig>, ) { // 计时器开始计时 config.timer.tick(time.delta()); if config.timer.finished() { commands .spawn() .insert(FuseTime { // 不重复的计时器 timer: Timer::new(Duration::from_secs(5), false), }); } } // 配置我们的炸弹生成算法 fn 炸弹生成设置( mut commands: Commands, ) { commands.insert_resource(BombsSpawnConfig { // 重复的计时器 timer: Timer::new(Duration::from_secs(10), true), }); } }
注意,Bevy 的计时器无法像典型的现实计时器那样工作(倒计时)。Bevy 的计时器从零开始计时,直到设定的“持续时间”(duration)。计时器就像秒表,但是多了额外的功能:最大持续时间、可选的“自动重置”。
秒表
todo:新概念。
Latest commit b6ba616
日志/控制台消息
运行 Bevy 项目时控制台会输出一些信息,例如:
2022-06-12T13:28:25.445644Z WARN wgpu_hal::vulkan::instance: Unable to find layer: VK_LAYER_KHRONOS_validation
2022-06-12T13:28:25.565795Z INFO bevy_render::renderer: AdapterInfo { name: "AMD Radeon RX 6600 XT", vendor: 4098, device: 29695, device_type: DiscreteGpu, backend: Vulkan }
2022-06-12T13:28:25.565795Z INFO mygame: Entered new map area.
日志信息有几个来源:Bevy 本身、依赖(例如 wgpu)、代码(例如调用了 info!())。
Bevy 提供了一个日志记录框架,该框架比简单地使用 Rust 的 println/eprintln 要先进得多。日志消息可以具有元数据,例如警告级别、时间戳、Rust 模块来源。元数据与消息的内容会一起打印出来。
级别
级别区分消息的重要程度,并允许过滤消息。
级别有:
error:报错了,程序无法运行warn:发生了不正常的事,程序可以运行info:一般的消息debug:用于开发过程的调试trace:用于非常冗长的调试数据,例如转储值
打印自定义日志消息
要显示消息,只需使用以消息级别命名的宏:
#![allow(unused)] fn main() { error!("Unknown condition!"); warn!("Something unusual happened!"); info!("Entered game level: {}", level_id); debug!("x: {}, state: {:?}", x, state); trace!("entity transform: {:?}", transform); }
过滤信息
用 LogSettings(配置类资源)来控制怎样过滤消息。
#![allow(unused)] fn main() { app.insert_resource(LogSettings { filter: "info,wgpu_core=warn,wgpu_hal=warn,mygame=debug".into(), level: bevy::log::level::DEBUG, }); }
todo:看不懂、未验证
Latest commit c8a3aaa
Hierarchical(Parent/Child) Entities
相关的官方文档:[hierarchy]、[parenting]
从技术上讲,实体/组件本身不能形成层次结构(它们是扁平数据结构)。但是,逻辑层次结构是游戏中常见的模式,Bevy 支持在实体之间建立有层次的逻辑联系,形成虚拟的 “层次结构”,方法是在实体上添加 Parent 组件和 Children 组件。
可以用 Commands 自带的方法给实体添加子实体,这些方法会自动添加正确的组件:
#![allow(unused)] fn main() { // 生成父实体并获取 ID let parent = commands.spawn_bundle(MyParentBundle::default()).id(); // 生成子实体并获取 ID let child = commands.spawn_bundle(MyChildBundle::default()).id(); // 把子实体添加到父实体,形成层级关系 commands.entity(parent).push_children(&[child]); // 也可以用 `with_children` commands.spawn_bundle(MyParentBundle::default()) .with_children(|child_builder| { cchild_builder.spawn_bundle(MyChildBundle::default()); }); }
可以用一个命令来销毁实体层级:
#![allow(unused)] fn main() { fn close_menu( mut commands: Commands, query: Query<Entity, With<MainMenuUI>>, ) { for entity in query.iter() { // 销毁实体和它的所有子级实体 commands.entity(entity).despawn_recursive(); } } }
访问父实体或子实体
要让系统处理具有层次结构的实体,要用到两个 Query:
- 一个查询子实体的组件
- 另一个查询父实体的组件
其中的一个查询应该包含合适的组件,才能获取所需的实体 ID:
- 如果在查询实体时要获取父实体的数据,就要用
Parent组件来获取父实体的 ID - 如果在查询实体时要获取子实体的数据,就要用
Children组件来获取子实体的 ID
例如,某个相机有一个父实体,需要获取相机的 Transform 和父实体的 GlobalTransform:
#![allow(unused)] fn main() { fn camera_with_parent( q_child: Query<(&Parent, &Transform), With<Camera>>, q_parent: Query<&GlobalTransform>, ) { for (parent, child_transform) in q_child.iter() { // `parent` 变量包含 Entity 的 ID,可以用这个 ID 来查询父实体的组件 let parent_global_transform = q_parent.get(parent.0); // 做一些事 } } }
再举一个例子,假设我们正在做一个战略游戏,游戏中的某个小分队(父实体)有一些成员(子实体)。为了使某个系统在每个小分队(父实体)上都有效,这个系统需要这些成员(子实体)的信息:
#![allow(unused)] fn main() { fn process_squad_damage( q_parent: Query<(&MySquadDamage, &Children)>, q_child: Query<&MyUnitHealth>, ) { // 获取每个小队的属性 for (squad_dmg, children) in q_parent.iter() { // `children` 是实体 ID 的集合 for &child in children.iter() { // 获取每个子实体单位的 “Health” let health = q_child.get(child); // 做一些事 } } } }
相对变换
如果实体代表“游戏世界中的对象”,那么子实体应该相对于父实体定位,并随父实体移动。
所有 Bevy 内置的 Bundle 自动提供“相对变换”功能。如果只要变换,不需要其他东西,可以使用 TransformBundle。
更多信息请查看[Transforms and Coordinates]。
变换和可见性传播
如果实体代表“游戏世界中的对象”,那么子实体应该受到父实体的影响:
- Transform:子实体相对于父实体定位,并随父实体移动
- Visibility:父实体的可见性会影响子实体的可见性
所有 Bevy 内置 Bundle 都是这样。自定义 Bundle 可以用 SpatialBundle 来添加 Transform 和 Visibility。
Latest commit 1d3f6e4
固定时间步长
相关的官方文档:[fixed_timestep]
如果要在固定的时间间隔内执行某些代码(例如物理更新),可以用 Bevy 的 FixedTimestep 运行标准。
use bevy::core::FixedTimestep; // 时间步长表示每秒运行 SystemSet 的次数 // 本例中,TIMESTEP_1 每秒一次,TIMESTEP_2 每秒两次 const TIMESTEP_1_PER_SECOND: f64 = 60.0 / 60.0; const TIMESTEP_2_PER_SECOND: f64 = 30.0 / 60.0; fn main() { App::new() .add_system_set( SystemSet::new() // 每秒打印一次 “hello world” .with_run_criteria(FixedTimestep::step(TIMESTEP_1_PER_SECOND)) .with_system(slow_timestep) ) .add_system_set( SystemSet::new() // 每秒打印两次 “goodbye world” .with_run_criteria(FixedTimestep::step(TIMESTEP_2_PER_SECOND)) .with_system(fast_timestep) ) .run(); } fn slow_timestep() { println!("hello world"); } fn fast_timestep() { println!("goodbye world"); }
状态
可以访问 FixedTimesteps 资源来检查固定时间步长跟踪器的当前状态,从而知道距离下次触发还剩多少时间,或者超过了多少时间。需要标记固定时间步长。
请查看官方示例。
注意事项
请注意,由于固定时间步长是用运行标准实现的,因此它不能与其他运行标准(例如状态)结合使用。这种冲突使得固定时间步长无法适用于大多数项目,因为这些项目需要依靠状态来实现主菜单、加载屏幕等功能。
todo:不太懂,大意是固定时间步长不准确 Also, note that your systems are still called as part of the regular frame-update cycle, along with all of the normal systems. So, the timing is not exact.
FixedTimestep 运行标准只是检查自上次运行系统以来经过的时间,并根据需要决定是否在当前帧内运行,或者多次运行。
危险!丢失事件!
默认情况下,Bevy 的事件是不可靠的:事件只持续 2 帧,之后就会丢失。如果用了固定时间步长的系统需要接收事件,请注意,帧率高于固定时间步长的 2 倍时,可能会错过某些事件。一个解决办法是手动清除时间,这样可以控制事件持续多长时间,但忘记清除事件会浪费内存甚至导致内存泄漏。
Latest commit a1d003d
声音
Bevy 内置音频功能有限,只有播放、控制音量。
TODO: show how to use Bevy audio, now that it its usability has improved.
Kira Audio
无第三方插件需求,略
TODO
Hot-Reloading Assets
热加载
在运行时,如果(用AssetServer)修改已被加载进游戏的资产文件,Bevy会检测到,并自动重载资产。这对于快速迭代非常有用,因为在游戏运行时编辑资产,会立即看到修改效果。
不是所有的文件格式和用例都得到同样好的支持。纹理/图像等典型资源类型应该可以正常工作,但复杂的GLTF或场景文件,或涉及自定义逻辑的资源可能无法正常工作。
如果要在热重载工作流中运行自定义逻辑,可以在系统中用[AssetEvent]实现。
热重新加载是可选的,必须启用才能工作。可以在启动系统中执行此操作:
#![allow(unused)] fn main() { asset_server.watch_for_change().unwrap(); }
Latest commit 4c0b78b
资产
相关的官方文档:asset_loading
Bevy用一个灵活的系统来异步加载和管理你的游戏资产(在后台,不会造成游戏的滞后高峰)。
加载资产时获得的数据存储在Assets<T>资源。
Bevy用句柄来跟踪资产。句柄只是特定资产的轻量级ID。
用AssetServer加载资产
用AssetServer资源从文件中加载资产。
#![allow(unused)] fn main() { struct UiFont(Handle<Font>); fn load_ui_font( mut commands: Commands, server: Res<AssetServer> ) { let handle: Handle<Font> = server.load("font.ttf"); // 可以把句柄存储在资源中,既可以防止资产被卸载,又能在以后访问 commands.insert_resource(UiFont(handle)); } }
这会使资产加载在后台进行排队,所以资产将需要一些时间才能使用。不能在相同的系统中立即访问实际数据,但是可以使用句柄。
可以在资产加载之前,就使用句柄生成2D精灵、3D模型和UI,当资产准备好后它们就会“弹”进来。
请注意,可以多次调用asset_server.load,即使资产正在加载或已经加载。它只是提供相同的句柄。如果资产不可用,它会开始加载。[TODO:不理解]
创建自定义资产
可以手动添加资产到Assets<T>。
Assets<T>可以用代码来创建资产(比如用于程序生成),或者用某些数据来创建资产。
#![allow(unused)] fn main() { fn add_material( mut material: ResMut<Assets<StandardMaterial>>, ) { let new_mat = StandardMaterial { base_color: Color::rgba(0.25, 0.50, 0.75, 1.0), unlit: true, ..Default::default() }; material.add(new_mat); } }
热加载
Bevy可以实时检测资源文件的改变,在游戏运行时重载资源。查看[Hot-Reloading Assets]。
句柄
句柄是引用特定资产的典型方法。 把事物(例如2D精灵,3D模型或UI)生成到游戏中时,它们各自的组件会需要它们所用资产的句柄。
可以把句柄存储在方便使用的地方(比如[资源])。
如果没有在任何地方存储句柄,可以通过调用asset_server.load从路径中生成句柄。如果不需要存储句柄,可以随时随地生成句柄。
知识点:
访问资产
用Assets<T>资源从系统中访问实际资产数据。
可以使用句柄或资产路径来标记所需的资产:
#![allow(unused)] fn main() { struct SpriteSheets { map_tiles: Handle<TextureAtlas>, } fn use_sprites( handles: Res<SpriteSheets>, atlases: Res<Assets<TextureAtlas>>, textures: Res<Assets<Texture>>, ) { // 如果资产未加载,返回`None` if let Some(atlas) = atlases.get(&handles.map_tiles) { // 做一些事 } // 可以用路径代替句柄 if let Some(map_tex) = textures.get("map.png") { // 做一些事 } } }
资产路径和标签
文件系统中的资产可以通过AssetPath来标记,AssetPath由文件路径+标签组成。标签用于同一文件中包含多个资产的情况。一个例子是GLTF文件,它可以包含meshes、场景、纹理、材质等。
资产路径可以由一个字符串创建,并在#符号后附加标签(如果有)。
#![allow(unused)] fn main() { fn load_gltf_things( mut commands: Commands, server: Res<AssetServer> ) { // 获取特定的网格 let my_mesh: Handle<Mesh> = server.load("my_scene.gltf#Mesh0/Primitive0"); // 生成整个场景 let my_scene: Handle<Scene> = server.load("my_scene.gltf#Scene0"); commands.spawn_scene(my_scene); } }
(对于GLTF,Bevy会根据文件中每个对象的索引生成像Scene0这样的标签,但如果GLTF文件包括从3D建模软件导出的名称/标签,Bevy也会提供这些标签) 有关使用三维模型的更多信息,请查看[GLTF]页面。
句柄和资产生命周期(垃圾收集)
句柄有内置的引用计数(类似于Rust中的Rc/Arc)。这使得Bevy可以跟踪一个资产是否还需要,并在不再需要时自动卸载。
可以使用.clone()来创建同一资产的多个句柄。克隆是一种廉价的操作,但它是显式的,以确保你知道代码中哪些地方会创建额外的句柄,并可能影响资产的生命周期。
弱句柄
句柄可以是“强”(默认)或“弱”。只计算强句柄并导致资产保持加载状态。弱句柄可以引用资产,同时允许它在没有强句柄时进行垃圾收集。
可以使用.clone_weak()代替.clone()来创建弱句柄。
无类型句柄
Bevy有HandleUntyped类型,用来引用任何类型的资产。
这允许存储一个包含混合类型资产的集合(如Vec或HashMap)。
用.clone_untyped()创建一个无类型句柄。
无类型加载
方便的是,如果不知道文件是什么资产类型,AssetServer支持无类型化加载。可以加载整个文件夹:
#![allow(unused)] fn main() { struct ExtraAssets(Vec<HandleUntyped>); fn load_extra_assets( mut commands: Commands, server: Res<AssetServer>, ) { if let Ok(handle) = server.load_folder("extra") { commands.insert_resource(ExtraAssets(handles)); } } }
它会尝试根据文件扩展名来检测每个资产的格式。[TODO:“它”指什么?]
资产事件
可以用AssetEvent在资产完成加载、修改或删除时执行特定的操作。
#![allow(unused)] fn main() { struct MapTexture { handle: Handle<Texture>, } fn fixup_textures( mut ev_asset: EventReader<AssetEvent<Texture>>, mut assets: ResMut<Assets<Texture>>, map_tex: Res<MapTexture>, ) { for ev in ev_asset.iter() { match ev { AssetEvent::Created { handle } | AssetEvent::Modified { handle} => { // 刚刚加载或更改了纹理 let texture = assets.get_mut(handle).unwrap(); // ^ unwrap返回Ok,因为当前已经加载成功 if *handle == map_tex.handle { // 这是特殊的地图纹理 } else { // 这是其他纹理 } } AssetEvent::Removed { handle} => { // 纹理被卸载 } } } } }
Latest commit 8000181
输入处理
相关的官方文档:Input Example
Bevy支持以下输入:
- 键盘(按下、释放)
- 字符(文本输入、操作系统处理的键盘布局)
- 鼠标
- 相对运动
- 指针位置
- 按键
- 滚轮
- 触摸屏(多点触控)
- 手柄/控制器/摇杆(通过 gilrs 库)
暂时不支持传感器(加速计、陀螺仪、VR 头部跟踪等)。
对于大多数输入类型(在有意义的地方),Bevy 提供了两种处理方法:
- 通过资源
- 或通过事件
有些输入仅作为事件提供。
检查状态是用 Input(数字式输入)、Axis(模拟式输入)或 Touch(触屏式输入)等资源来完成的:
- 数字式,包含“按下”或“释放”两种状态,获取其中某个状态
- 模拟式,包含一个范围,获取这个范围的某个值
- 触屏式,[TODO]
事件是一种更低级、更全面的方法。如果想从输入设备获得所有输入活动,就用事件,而不是仅仅检查特定的输入。
输入映射
Bevy 还没有内置“输入映射”功能。
社区制作的插件可能有用:Bevy Assets - input#。
Latest commit 0f209fc
键盘输入
相关的官方文档:[keyboard_input]、[keyboard_input_events]
检查某个键的状态
检查某个键的状态(按下或释放)目前只能通过键码(Key Code)来完成,用的是 Input<KeyCode> 资源:
#![allow(unused)] fn main() { fn 键盘输入( input: Res<Input<KeyCode>>, ) { if input.just_pressed(KeyCOde::Space) { // 按下空格键 } if input.just_released(KeyCode::LControl) { // 释放左 Ctrl 键 } if input.pressed(KeyCode::W) { // 按下 W 键 } // 用 `.any_*` 来检测多个输入 if input.any_pressed([KeyCode::LShift, KeyCode::RShift]) { // 按住左 Shift 键或右 Shift 键 } if input.any_just_pressed([KeyCode::Delete, KeyCode::Back]) { // 按下退格键或删除键 } } }
键盘事件
要获取所有键盘活动,可以用 KeyboardInput 事件:
#![allow(unused)] fn main() { use bevy::input::keyboard::KeyboardInput; fn 键盘事件( mut event_reader: EventReader<KeyboardInput>, ) { use bevy::input::ButtonState; for event in event_reader.iter() { match event.state { ButtonState::Pressed => { info!("按下{:?}键({})", event.key_code, event.scan_code); } ButtonState::Released => { info!("释放{:?}键({})", event.key_code, event.scan_code); } } } } }
键盘事件返回键码(Key Code)和扫描码(Scan Code),扫描码是一个 u32 类型的整数 ID。
键码和扫描码
可以通过键码或扫描码来识别键盘的按键。
键码代表每个键上的符号或字母,取决于键盘布局。Bevy 用 KeyCode 枚举来表示键码。
扫描码表示键盘上的物理按键,与键盘布局无关。Bevy 对键盘扫描码的支持有限。issue:Improve keyboard input with Input
Layout-Agnostic Key Bindings
todo:不了解扫描码
Latest commit 6ac219a
鼠标
相关的官方文档:[mouse_input]、[mouse_input_events]
鼠标按键
鼠标按键的状态和事件与键盘输入类似。
用 Input<MouseButton> 检查某个鼠标按键的状态(按下或释放):
#![allow(unused)] fn main() { fn 鼠标按键输入( input: Res<Input<MouseButton>>, ) { if input.just_pressed(MouseButton::Left) { // 按下鼠标左键 } if input.just_released(MouseButton::Left) { // 释放鼠标左键 } if input.pressed(MouseButton::Right) { // 按住鼠标右键 } // 用 `.any_*` 来检测多个输入 if input.any_just_pressed([MouseButton::Left, MouseButton::Right]) { // 按下鼠标左键或鼠标右键 } } }
要获取所有鼠标按键活动,可以用 MouseButtonInput 事件:
#![allow(unused)] fn main() { use bevy::input::mouse::MouseButtonInput; fn 鼠标按键事件( mut event_reader: EventReader<MouseButtonInput>, ) { use bevy::input::ButtonState; for event in event_reader.iter() { match event.state { ButtonState::Pressed => { info!("按下鼠标按钮:{:?}", event.button); } ButtonState::Released => { info!("释放鼠标按钮:{:?}", event.button); } } } } }
鼠标滚轮
用 MouseWheel 事件检测滚动输入:
#![allow(unused)] fn main() { use bevy::input::mouse::MouseWheel; fn 鼠标滚轮事件( mut event_reader: EventReader<MouseWheel>, ) { use bevy::input::mouse::MouseScrollUnit; for event in event_reader.iter() { match event.unit { MouseScrollUnit::Line => { info!("滚动(行单位):垂直:{},水平:{}", event.y, event.x); } MouseScrollUnit::Pixel => { info!("滚动(像素单位):垂直:{},水平:{}", event.y, event.x); } } } } }
MouseSrollUnit 枚举很重要,它是鼠标滚轮输入的类型。行单位类型用于具有固定步数的硬件,例如桌面鼠标上的滚轮。像素单位类型用于平滑(细粒度)滚动的硬件,例如笔记本电脑的触摸板。
鼠标运动
鼠标的相对运动一般用在 3D 摄像机,只关心鼠标在一帧内移动了多少单位,而不关心鼠标指针的准确位置。
要检测鼠标的相对运动,可以用 MouseMotion 事件。每当鼠标移动时,会收到一个带有 delta 的事件:
#![allow(unused)] fn main() { use bevy::input::mouse::MouseMotion; fn 鼠标运动( mut event_reader: EventReader<MouseMotion>, ) { for event in event_reader.iter() { info!("鼠标移动了:X:{}像素,Y:{}像素", event.delta.x, event.delta.y); } } }
相关阅读:grab/lock the mouse inside the game window
鼠标指针位置
可以从相应的 Window 获取鼠标指针的当前坐标:
#![allow(unused)] fn main() { fn 鼠标指针位置( windows: Res<Windows>, ) { // 游戏通常只有一个窗口(主窗口) // 对于多窗口应用程序,要在这里指定窗口 ID let window = windows.get_primary().unwrap(); if let Some(_position) = window.cursor_position() { // 鼠标指针在窗口内 } else { // 鼠标指针不在窗口内 } } }
要检测指针是否移动,可以用 CursorMoved 事件获取更新后的坐标:
#![allow(unused)] fn main() { fn 鼠标指针事件( mut event_reader: EventReader<CursorMoved>, ) { for event in event_reader.iter() { info!("新的鼠标指针位置:X:{},Y:{},窗口 ID:{}", event.position.x, event.position.y, event.id); } } }
请注意,只能获取鼠标在窗口内的位置,无法在整个系统桌面或屏幕上获得鼠标的全局位置。获得的是“窗口空间”中的坐标,表示窗口像素,原点在窗口左下角,与相机或游戏中的坐标系没有任何关系。关于窗口鼠标指针坐标转换为世界坐标,查看 Convert cursor to world coordinates。
要检测鼠标指针进入或离开窗口,可以用 CursorEntered 和 CursorLeft 事件。
Latest commit c7956e8
修改背景颜色
相关的官方文档:clear_color
用 ClearColor 资源来设定背景颜色。
fn main() { App::new() .insert_resource(ClearColor(Color::rgb(0.4, 0.4, 0.4))) .add_plugins(DefaultPlugins) .run(); }
Latest commit 85dac0c
ECS as a Data Structure
相关的官方文档:[ecs_guide]
Bevy 用 Bevy ECS 来储存和管理数据。
可以把 ECS 比作数据库或电子表格,不同的数据类型(组件)就像表格的 “列”,可以有任意多的“行”(实体),“行”包含各种组件的值或实例。
如果游戏中有许多表示不同事物(玩家、NPC、怪物)的实体,那么创建一个 Health 组件之后,所有这些实体都可以添加 Health 组件。编写游戏逻辑(系统)变得简单:只用必要的组件来操作实体,例如,涉及“伤害”时,只需用 Health 组件即可。游戏逻辑变得非常灵活和可重用。
一个实体所拥有的组件的集合(组合),称为该实体的原型。
注意,实体不仅限于“游戏世界中的对象”。ECS 是一个通用的数据结构,实体和组件可以存储任何数据。
性能
Bevy有智能调度算法,会尽量并行运行系统,前提是系统能安全地访问数据(无数据竞争)。因此无需额外的开发工作,游戏就能多核心运行你的项目。
把数据分割成更小的类型(组件),把逻辑分割成多个更小的函数(系统),可以提高并行运行的机会。数据与系统的相关性越大,系统访问数据时冲突就越少,游戏运行速度就越快。
Bevy 对性能的经验法则是:颗粒度越细越好。
面向对象语言出身的程序员须知
你可能习惯用“对象类”来思考,例如,你可能定义一个包含 “player” 所有字段或属性的大型 Player 结构体。在 Bevy 中,这是不好的做法,会增加数据处理的难度,并限制性能。相反,当不同的数据可能被独立访问时,你应该把事情细化。例如,把游戏中的 “Player” 表示成由组件组成的实体,组件可以是“生命值”、“经验值”等,也可以是 Bevy 内置组件(例如 Transform)。
但是像 Transform 或坐标等,不应过度细化,它们作为单独的结构体才有意义,因为它们的字段不能独立地发挥作用。
Latest commit febb142
实体和组件
相关的官方文档:[ecs_guide]
实体
实体是一个整数 ID,这个 ID 用来标识一组特定的组件数值。
用 Commands 来创建新的实体。
组件
组件是与实体关联的数据。创建新的组件:定义一个 Rust 结构体或枚举,然后派生 Component 特型。
#![allow(unused)] fn main() { #[derive(Component)] struct Health { hp: f32, extra: f32, } }
类型必须是唯一的:每个 Rust 类型只能对应实体内的一个组件。
用 wrapper(newtype 模式)结构把简单的类型变成独特的组件:
#![allow(unused)] fn main() { #[derive(Component)] struct PlayerXp(u32); #[derive(Component)] struct PlayerName(String); }
用空结构体来标记特定的实体。空结构体被称为“标记组件”,对查询过滤器很有用。
#![allow(unused)] fn main() { // 把空结构体 MainMenuUI 添加到所有的“菜单界面”实体中,以后可以用 MainMenuUI 来查询所有“菜单界面”实体 #[derive(Component)] struct MainMenuUI; // 用来标记“玩家” #[derive(Component)] struct Player; // 用来标记“敌人” #[derive(Component)] struct Enemy; }
可以用 Query 在系统中访问组件。可以用 Commands 添加/移除实体中的组件。
组件包
包就像“模板”一样,能轻松创建内含通用组件的实体。
#![allow(unused)] fn main() { #[derive(Bundle)] struct PlayerBundle { xp: PlayerXp, name: PlayerName, health: Health, _p: Player, // 可以嵌套或包含另一个 Bundle // 添加一个用于 Bevy Sprite 的组件: #[bundle] sprite: SpriteSheetBundle, } }
Bevy 还把任何由组件组成的元组视为 Bundle:
#![allow(unused)] fn main() { (ComponentA, ComponentB, ComponentC) }
使用 Query 时需注意:无法查询整个组件包,只能查询“组件”,组件包有什么组件,就去查询里面包含的组件。
Latest commit d7a897d
资源
相关的官方文档:[ecs_guide]
资源可以存储某种数据类型的唯一全局实例,独立于实体(与实体无关)。
资源用于程序中真正的全局数据,例如配置、设置。
任何 Rust 类型(结构体或枚举)都可以成为资源。
类型必须是唯一的;给定类型只能有一个资源实例。
#![allow(unused)] fn main() { struct GoalsReached { main_goal: bool, bonus: bool, } }
可以用 Res 或 ResMut 在系统中访问资源。
资源初始化
给简单的资源实现 Default:
#![allow(unused)] fn main() { #[derive(Default)] struct StartingLevel(usize); }
复杂资源的初始化需要实现 FromWorld:
#![allow(unused)] fn main() { struct MyFancyResource { /* stuff */ } impl FromWorld for MyFancyResource { fn from_world(world: &mut World) -> Self { // 可以在这里完全地访问 ECS 中的任何内容 // 例如,可以可变地访问其他资源 let mut x = world.get_resource_mut::<MyOtherResource>().unwrap(); x.do_mut_stuff(); MyFancyResource { /* stuff */} } } }
可以在构建 [App] 时初始化资源:
fn main() { App::new() // ... // 如果资源实现了 `Default` 或 `Fromworld`: .init_resource::<MyFancyResource>() // 如果没有实现上面的特型,或者想要设置一个具体的值: .insert_resource(StartingLevel(3)) // ... .run(); }
或者从系统内部用 [Commands] 来创建/移除资源:
#![allow(unused)] fn main() { commands.insert_resource(GoalReached { main_goal: false, bonus: false}); commands.remove_resource::<MyResource>(); }
插入一个已存在的资源类型时,新值会覆盖旧值。
使用建议
是用“实体/组件”还是使用资源取决于你想如何访问数据:从任何地方(资源),或用 ECS 模式(实体/组件)。
即使游戏中某些东西只有一个(比如单机游戏中的玩家),用实体也比用资源更合适,因为实体由多个组件组成,而组件可以和其他实体通用。这可以使游戏逻辑更加灵活。例如,可以设计一个对玩家和敌人都有效的“生命值/伤害系统”。
Latest commit d7a897d
系统
相关的官方文档:[ecs_guide],[startup_system],[system_param]
用 Rust 编写的函数,经由 Bevy 运行后就是系统,是实现游戏逻辑的地方。
这些函数只能接受特殊的参数类型来指定要访问的数据。如果使用了不支持的参数类型,编译时会报错,查看[Error adding function as system]。这些参数类型是:
Res/ResMut:访问资源;Query:访问实体的组件;Commands:创建或销毁实体、组件和资源;EventWriter/EventReader:发送或接收事件。
#![allow(unused)] fn main() { fn debug_start( // 访问资源 start: Res<StartingLevel> ) { eprintln!("Starting on level {:?}", *start); } }
可以把系统的参数组合成元组(嵌套)。
#![allow(unused)] fn main() { fn comples_system( (a, mut b): (Res<ResourceA>, ResMut<ResourceB>), // 这个资源可能不存在,所以把它包在一个 Option 里 mut c: Option<ResMut<ResourceC>>, ) { lf let Some(mut c) = c { // 做一些事 } } }
顶层系统参数或元组成员的最大数量是 16 个。如果要绕过限制,可以把它们分组或嵌套成元组。
运行时
用 [App Builder] 把系统添加到 Bevy 即可运行系统:
fn main() { App::new() // ... // 仅运行一次 .add_startup_system(init_menu) .add_startup_system(debug_start) // 每帧运行一次 .add_system(move_player) .add_system(enemies_ai) // ... .run(); }
随着游戏项目越来越复杂,需要用更多功能来设置 App Buidler,管理系统“什么时候”和“如何”运行,例如:用Label、System Set、State、Run Criteria和Stage来确定[系统的执行顺序]。
Latest commit 209b722
查询
相关的官方文档:[ecs_guide]
Query 能访问实体的组件。
#![allow(unused)] fn main() { fn check_zero_health( // 访问带有 `Health` 和 `Transform` 组件的实体 // 获取对 `Health` 的只读访问和对 `Transform` 的可变访问 // 可选组件:如果 `Player` 存在,获取只读访问 mut query: Query<(&Health, &mut Transform, Option<&Player>)>, ) { // 获取所有匹配的实体 for (health, mut transform, player) in query.iter_mut() { eprintln!("Entity at {} has {} HP.", transform.translation, health.hp); // 读取 `Health`,修改 `Transform` if health.hp <= 0.0 { transform.translate = Vec3::ZERO; } // 判断 `Player` 是否存在 if let Some(player) = player { // 做一些事 } } } }
获取与特定实体关联的组件:
#![allow(unused)] fn main() { if let Ok((health, mut transform)) = query.get_mut(entity) { // 做一些事 } else { // 实体中没有 query 要求的组件 } }
获取Query访问的实体的 ID:
#![allow(unused)] fn main() { // 把 `Entity` 添加到 `Query` 来获取 Entity 的 ID fn query_entites(q: Query<(Entity, /* ... */)>) { for (e, /* ... */) in q.iter() { // `e` 就是我们正在访问的 Entity 的 ID } } }
如果明确地知道 Query 只匹配一个实体,可以用 single 或 single_mut(返回 Result),而不是 iter 或 iter_mut:
#![allow(unused)] fn main() { fn query_player(mut q: Query<(&Player, &mut Transform)>) { let (player, mut transform) = q.single_mut().expect("单机游戏只有一个玩家!"); // 做一些事 } }
(如果 single 查询到多个实体,会 panic!)
包
Query 查询的是实体的组件,如果某个实体从 Bundle 创建,那么在 Query 中不要查询 Bundle,要查询 Bundle 中的组件。记住:查询“组件”。
查询过滤器
过滤器的作用是缩小查询范围。用 With 或 Without 来获取带有特定组件的实体。
#![allow(unused)] fn main() { fn debug_player_hp( // 访问 `Health`,但只针对队友。可选:具有名字 query: Query<(&Health, Option<&PlayerName>), (With<Player>, Without<Enemy>)>, ) { // 获取所有匹配的实体 for (health, name) in query.iter() { if let Some(name) = name { eprintln!("Player {} has {} HP.", name.0, health.hp); } else { eprintln!("Unknown player has {} HP.", health.hp); } } } }
可以组合多个过滤器:
- 元组中包含多个过滤器:所有过滤器起作用(逻辑与)。
Or<...>中包含多个过滤器:过滤器其中之一起作用(逻辑或)。- (注意元组内部)
Latest commit d7a897d
命令
相关的官方文档:[ecs_guide]
Comands 的作用:
- 生成或销毁实体
- 给实体添加或移除组件
- 管理资源
Command 不是立即生效,而是排队等待,在安全时执行。查看 [Stage]。(如果不用 Stage,系统会在[下一帧更新]时看到它们。)
#![allow(unused)] fn main() { fn spawn_player(mut commands: Commands) { // 管理资源 commands.insert_resource(GoalsReached { main_goal: false, bonus: false, }); commands.remove_resource::<MyResource>(); // 用 spawn 生成新的实体 let entity_id = commands .spawn() // 添加组件 .insert(ComponentA) // 添加Bundle .insert_bundle(MyBundle::default()) // 获取实体的 ID .id(); // 用 Bundle 创建实体 commands.spawn_bundle(PlayerBundle { name: PlayerName("Henry".into()), xp: PlayerXp(1000), health: Health { hp: 100.0, extra: 20.0, }, _p: Player, sprite: Default::default(), }); // 生成另一个实体 // 注意:组件组合成元组就变成了 Bundle let other = commands .spawn_bundle(( ComponentA::default(), ComponentB::default(), ComponentC::default(), )) .id(); // 给实体添加或移除组件 commands .entity(entity_id) .insert(ComponentB) .remove::<ComponentA>() .remove_bundle::<MyBundle>(); // 销毁实体 commands.entity(other).despawn(); } fn make_all_players_hostile(mut commands: Commands, query: Query<Entity, With<Player>>) { for entity in query.iter() { // 给实体添加一个 Enemy 组件 commands.entity(entity).insert(Enemy); } } }
注意不要混淆 Component 和 Bundle。例如:
.insert_bundle 与 Bundle 组合使用,会正确地从 Bundle 中添加所有组件;.insert 和 Bundle 组合使用,只会把 Bundle 作为单个组件添加。
Latest commit 85dac0c
事件
相关的官方文档:[event]
在系统间发送数据:
EventWriter<T>:发送事件EventReader<T>:接收事件
每个Reader独立地追踪已读取的事件,因此可以处理来自多个系统的事件。
#![allow(unused)] fn main() { struct LevelUpEvent(Entity); fn player_level_up( mut ev_levelup: EventWriter<LevelUpEvent>, query: Query<(Entity, &PlayerXp)>, ) { for (entity, xp) in query.iter() { if xp.0 > 1000 { ev_levelup.send(LevelUpEvent(entity)); } } } fn debug_levelups( mut ev_levelup: EventReader<LevelUpEvent>, ) { for ev in ev_levelup.iter() { eprintln!("Entity {:?} level up!", ev.0); } } }
自定义事件要先在 [App Builder] 中注册:
fn main() { App::new() // ... .add_event::<LevelUpEvent>() .add_system(player_level_up) .add_system(debug_levelups) // ... .run(); }
事件应该是首选的数据流工具,因为事件可以从任何系统发送,并由多个系统接收,用途广泛。
隐藏的陷阱
注意 [Avoiding Frame Delays / 1-frame-lag]。如果Bevy在发送事件的系统之前先运行了接收事件的系统,就会出现这种情况。接收事件的系统只能在下一帧更新时接收事件。如果要在发送事件的同一帧立即接收事件,可以用 [System Order of Execution]。
事件不能持久。事件的储存时间是当前帧到下一帧结束前,之后就会丢失。如果某个处理事件的系统不是每帧运行,就会错过一些事件。这种设计好处是不用担心内存浪费在未处理的事件上。
如果希望事件能持久,可以[手动控制何时清除事件](忘记清除事件会浪费内存,甚至导致内存泄露)。
Latest commit 85dac0c
App Builder (main function)
相关的官方文档:[ecs_guide],[hello_world],[empty],[empty_defaults],[headless]
配置好 [App] 后才能进入 Bevy 的运行时。这个 App 可以由[插件]、[系统]、[事件]、[状态]和[阶段]组成。
App 包含了 “ECS 世界”(World,保存了所有数据,也就是组件)和调度(Schedule,保存了所有的系统)。[Sub-apps] 是高级用法,用来生成多个 “ECS 世界”和“调度”。
[本地资源] 无需注册,它们是所属系统的一部分。[组件]类型无需注册。
调度(目前)不能再运行时修改;所有系统都必须在 App 中注册,否则无法使用。
ECS 世界的数据可以在任何时候修改;在系统中,可以用 Commands 来创建/销毁实体或资源,可以用 todo 来 todo。
可以在 App 中提前初始化资源。
还要添加Bevy的内置功能:如果要做一个完整的游戏(或应用程序),用 DefaultPlugins;如果是无头服务器之类的,用 MinimalPlugins。
注意,todo。
fn main() { App::new() // 配置类的资源要优先添加,甚至要比 Bevy 本身早 .insert_resource(WindowDescriptor { // ... ..default() }) // Bevy 本身 .add_plugins(DefaultPlugins) // 资源 .insert_resource(StartingLevel(3)) // 可以为资源实现 `Default` 或 `FromWorld`,表示这些资源能够在创建时初始化 .init_resource::<MyFancyResource>() // 事件 .add_event::<LevelUpEvent>() // 这个系统在 App 启动时只运行一次 .add_startup_system(spawn_player) // 这个系统在 App 启动后每帧运行 .add_system(player_level_up) .add_system(debug_levelups) .add_system(debug_stats_change) // ... // 启动应用程序 .run(); }
Latest commit a1d003d
退出程序
在任何系统里发送 AppExit 事件来彻底关闭 Bevy 程序。
#![allow(unused)] fn main() { use bevy::app::AppExit; fn 退出程序( mut exit: EventWriter<AppExit>, ) { exit.send(AppExit); } }
为了便于开发原型,Bevy 提供了一个简单的系统,添加到 App 后,只要按下 ESC 键就能直接退出程序(先关闭聚焦窗口,所有窗口关闭后,再退出程序):
fn main() { App::new() .add_plugins(DefaultPlugins) .add_system(bevy::input::system::exit_on_esc_system) .run(); }
Latest commit 6f15887
本地资源
相关的官方文档:[ecs_guide]
什么是本地资源
本地资源就是“仅供当前系统使用的数据”。
Local<T> 是一个类似 ResMut<T>的系统参数,能可变地访问某个数据类型的实例(这个实例独立于实体和组件)。
资源与本地资源的区别
Res<T> 和 ResMut<T> 指的是 T 类型的单一全局实例,所有系统共享;每个 Local<T> 参数都是一个单独的实例,专属于使用 Local<T> 的系统。
#![allow(unused)] fn main() { #[derive(Default)] struct MyState; fn my_system1(mut local: Local<MyState>) { // 做一些事 } fn my_system2(mut local: Local<MyState>) { // 这个 locl 变量与 my_system1 的 locl 变量不同 } }
Local<T> 类型必须实现 Default 或 FromWorld。Local 会自动初始化。
系统可以拥有多个相同类型的 Local。
指定初始值
Local<T> 总是用默认的数值来自动初始化。
如果需要特殊的数据,可以用闭包代替。todo。使用闭包可以将数据“移动到函数中”。
此示例显示了如何在不使用 Local<T> 的情况下初始化某些数据以配置系统:
#[derive(Default)] struct MyConfig { magic: usize, } fn my_system( mut cmd: Commands, my_res: Res<MyStuff>, // 请注意,这不是有效的系统参数 config: &MyConfig, ) { // TODO: 做一些事 } fn main() { let config = MyConfig { magic: 420, }; App::new() // 创建一个闭包来使用 config 变量 .add_system(move |cmd: Commands, res: Res<MyStuff>| { // 从闭包内部调用函数 my_system(cmd, res, &config); }) .run(); }
Latest commit d7a897d
插件
相关的官方文档:[plugin],[plugin_group]。
把要添加到 [App Builder] 的东西组合起来就是插件。
struct MyPlugin; impl Plugin for MyPlugin { fn build(&self, app: &mut App) { app .init_resource::<MyResource>() .add_event::<MyEvent>() .add_startup_system(plugin_init) .add_system(my_system); } } fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugin(MyPlugin) .run(); }
[TODO] 对于你自己项目的内部组织,插件的主要价值在于不必声明你所有的 Rust 类型和函数为 pub,只是为了它们可以从 main 访问并添加到 app builder 中。插件可以让你从多个不同的地方添加东西到你的应用程序中,比如单独的 Rust 文件/模块。
插件可以融入你的游戏框架。一些建议:
- 为不同的 [State] 创建插件。
- 为各种子系统创建插件,例如物理处理、输入处理等。
插件组
插件组能一次注册多个插件,例如 Bevy 的 DefaultPlugins 和 MinimalPlugins。也可以创建自定义插件组:
struct MyPluginGroup; impl PluginGroup for MyPluginGroup { fn build(&mut self, group: &mut PluginGroupBuilder) { group .add(FooPlugin) .add(BarPlugin); } } fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugins(MyPluginGroup) .run(); }
添加插件组时,可以在禁用一些插件的同时保留其他插件。例如,想自定义日志记录(替换掉默认的日志记录),可以禁用 LogPlugin:
#![allow(unused)] fn main() { App::build() .add_plugins_with(DefaultPlugins, |plugins| { plugins.disable::<LogPlugin>() }) .run(); }
注意,禁用功能不会删除代码来避免二进制膨胀,禁用的插件仍然会编译进程序。如果想精简构建文件,可以禁用 Bevy 默认的 Cargo 功能,或者单独依赖 Bevy 的子 Crate。
发布 Crate
想把插件发布成Crate,可以参考 Third Party Plugin Guidelines。
Latest commit 1970b9e
系统执行顺序
Bevy 的调度算法会尽量多线程地运行多个系统,前提是不发生数据竞争。当一个系统可变地访问某个数据,而另一个系统也访问同一数据,那么这两个系统就不能同时运行。Bevy 可以从系统的参数知道是可变访问还是不可变访问。
默认情况下,系统执行顺序不确定。Bevy 不关心这些系统什么时候运行,甚至每一帧都会有不同的系统执行顺序!
这里有什么问题?
多数情况下,无需担心系统执行顺序。但是有时要让系统按特定顺序执行:
- 涉及数据修改顺序:一个系统修改数据,而另一个系统访问数据。
- 涉及事件:一个系统发送事件,另一个系统接收事件。
- 正在使用 [Change Detection]。
系统以错误的顺序运行会导致某些行为延迟到下一帧(查看[Avoiding Frame Delays / 1-frame-lag]),甚至产生严重的 bug。系统执行顺序是否重要取决于具体游戏逻辑。偏重视觉的游戏可能并不在乎延迟一帧,那么默认的系统执行顺序(顺序不确定)会有更好的性能。偏重操作的游戏则要重视延迟问题,要考虑系统执行顺序。
显式系统排序
明确指定系统执行顺序:
fn main() { App::new() .add_plugins(DefaultPlugins) // 这些系统不关心执行顺序 .add_system(particle_effects) .add_system(npc_behaviors) .add_system(enemy_movement) .add_system(input_handling) .add_system( player_movement // player_movement 总是在 enemy_movement 之前执行 .before(enemy_movement) // player_movement 总是在 input_handling 之后执行 .after(input_handling) ) .run(); }
.before/.after 可以在一个系统上多次使用。
标签
标签是高级用法。标签可以是字符串(String),也可以是任何实现了 SystemLabel 的自定义类型。
可以在同一个系统上设置多个标签(.label、.before、.after),也可以在多个系统上设置同一个标签。
每个标签都是参考点,系统可以根据标签来排序。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum MyLabel { Input, Player, } fn main() { App::new() .add_plugins(DefaultPlugins) // 创建标签 .add_system(input_joystick.label(MyLabel::Input)) .add_system(input_keyboard.label(MyLabel::Input)) .add_system(input_touch.label(MyLabel::Input)) // 这个系统总是在被 input 标签标记的系统之前执行 .add_system(input_parameters.before(MyLabel::Input)) // 这个系统总是在被 input、map 标签标记的系统之后执行 // 同样加一些标签 .add_system( player_movement // 也可以用字符串作为标签 .label("player_movement") .label(MyLabel::Player) .label(MyLabel::Input) ) .run(); }
多个系统拥有共同的标签或顺序时,可以用 [System Sets]。
循环依赖
[TODO:看不懂] If you have multiple systems mutually depending on each other, then it is clearly impossible to resolve the situation completely like that.
You should try to redesign your game to avoid such situations, or just accept the consequences. You can at least make it behave predictably, using explicit ordering to specify the order you prefer.
Latest commit d7a897d
系统集
系统集能够方便地把通用的属性应用到多个系统。一般用来设置 Label、System Order of Execution、Run Criteria 和 State。
fn main() { App::new() .add_plugins(DefaultPlugins) // 把与 input 有关的系统归为一个集合 .add_system_set( SystemSet::new() .lebel("input") .with_system(keyboard_input) .with_system(gamepad_input) ) // 被 net 标记的系统会在被 input 标记的系统之前执行 .add_system_set( SystemSet::new() .label("net") .before("input") // 在 System Set 中,各个系统仍然可以有自己的标签或执行顺序 .with_system(servel_session.label("session")) .with_system(servel_updates.after("session")) ) // 未分组的系统 .add_system(player_movement.after("input")) .add_system(session_ui.after("session")) .add_system(smoke_particles) .run(); }
Latest commit d38037d
变化检测
相关的官方文档:[change_detection]
Bevy 能够检测数据变化。可以在数据的变化时执行某些操作。ECS 中组件储存了数据,检测数据也就是检测组件。
todo:没用过,不懂 大意是其中一个用例是优化,仅在数据变化时执行相关操作,避免不必要的操作;另一个用例是配置内容或发送数据。
组件
过滤器
用 Query 来检测特定组件的变化情况。
使用 [Query Filters]:
-
Added<T>:检测新的组件实例- 如果把组件添加到实体中
- 如果带有组件的实体被创建
-
Changed<T>:检测组件实例是否被修改- 组件被可变地访问
- 组件刚被添加(
Added)
(如果想检测删除行为,请查看 [Removal Detection]。删除检测的工作方式不同,使用起来也比较麻烦。)
#![allow(unused)] fn main() { /// 当队友发生改变时打印队友的统计数据 fn debug_states_change( query: Query< // 组件 (&Health, &PlayerXp), // 过滤器 (Without<Enemy>, Or<(Changed<Health>, Changed<PlayerXp>)>), >, ) { for (health, xp) in query.iter() { eprintln!( "hp: {}+{}, xp: {}", health.hp, health.extra, xp.0 ); } } /// 检测新的敌人,并打印敌人的 Health fn debug_new_hostiles( query: Query<(Entity, &Health), Added<Enemy>>, ) { for (entity, health) in query.iter() { eprintln!("Entity {:?} is now an enemy! HP: {}", entity, health.hp); } } }
检测
todo
资源
对于资源,可以通过 Res/ResMut 的方法来检测变化。
#![allow(unused)] fn main() { fn check_res_changed( my_res: Res<MyResource>, ) { if my_res.is_changed() { // 做一些事 } } fn check_res_added( // 用到 Option,防止资源不存在时 panic! my_res: Option<Res<MyResource>>, ) { if let Some(my_res) = my_res { // 资源存在 if my_res.is_added() { // 资源刚刚被添加,做一些事 } } } }
注意,目前变化检测不能检测 state 的变化,是个 bug。
检测到什么?
todo:不懂啊
changed 的检测由 DerefMut触发。仅仅通过可变的查询来访问组件,而不实际通过 &mut 来访问组件,就不会触发检测。这样能让变化检测非常准确。变化检测可以用来优化游戏性能,或者触发其他逻辑行为。
注意:可变地访问组件时,Bevy 不会跟踪新值是否与旧值不同,而是一直触发变化检测。如果要在值变化时触发检测,可以手动实现:
#![allow(unused)] fn main() { fn update_player_xp( mut query: Query<&mut PlayerXp>, ) { for mut xp in query.iter_mut() { let new_xp = maybe_lvl_up(&xp); // 只在数值不同时触发变化检测 if new_xp != *xp { *xp = new_xp; } } } }
变化检测是可靠的,因为它会检测自上次检测系统运行以来发生的任何变化。 如果某个系统只是有时运行(用了 [States] 或 [Run Criteria]),不用担心会错过变化检测。
可能存在的陷阱
小心 [Avoiding Frame Delays / 1-frame-lag]。如果 Bevy 在运行“修改数据”的系统之前运行“检测数据变化”的系统,就会发生这种情况。,这时检测系统会在下一帧更新时得知数据被修改。如果要在同一帧处理“检测”和“修改”,可以用 [System Order of Execution]。但是,如果用 Added<T> 来检测组件的添加(通常是 Commands),还要使用 [Stages]。
Latest commit 1d3f6e4
状态
相关的官方文档:[state]
状态可以控制应用程序的运行时“流程”。状态可以做这些事:
- 制作菜单画面或加载画面
- 暂停(或取消暂停)游戏
- 不同的游戏模式
- ...
不同的状态可以运行不同的系统。还可以添加一次性(one-shot)设置和清理(cleanup)系统,以便在进入或退出某个状态时执行。
要使用状态,先要定义一个枚举类型,并把 [System Sets] 添加到 [App Builder]:
#[derive(Debug, Clone, Eq, PartialEq, Hash)] enum AppState { MainMenu, InGame, Paused, } fn main() { App::new() .add_plugins(DefaultPlugins) // 添加 `AppState` 类型 .add_state(AppState::MainMenu) // 默认添加的系统运行在所有状态下(与状态无关) .add_system(play_music) // 这个系统在“更新” `AppState::MainMenu` 状态时运行 .add_system_set( SystemSet::on_update(AppState::MainMenu) .with_system(handle_ui_buttons) ) // 这个系统在“进入” `AppState::MainMenu` 状态时运行 .add_system_set( SystemSet::on_enter(AppState::MainMenu) .with_system(setup_menu) ) // 这个系统在“离开” `AppState::MainMenu` 状态时运行 .add_system_set( SystemSet::on_exit(AppState::MainMenu) .with_system(close_menu) ) .run(); }
同一状态可以有多个系统集,这样放置[标签]和使用[显式系统排序]时更方便。
状态对[插件]也有用,每个插件都可以把自己的系统集添加到同一个状态中。
状态是用 [Run Criteria] 来实现的。这些特殊的系统集构造函数实际上只是帮助程序自动添加状态管理运行条件(todo:不理解)。
控制状态
在系统里,可以用 State<T> 资源来检查和控制状态:
#![allow(unused)] fn main() { fn play_music( app_state: Res<State<AppState>>, // ... ) { match app_state.current() { AppState::MainMenu => { // TODO: play menu music } AppState::InGame => { // TODO: play game music } AppState::Paused => { // TODO: play pause screen music } } } }
切换到另一个状态:
#![allow(unused)] fn main() { fn enter_game(mut app_state: ResMut<State<AppState>>) { app_state.set(AppState::InGame).unwrap(); // ^ 如果当前状态已经是目标状态,或者已经有另一个“切换状态”在排队,会报错 } }
当前状态的所有系统都执行完毕后,Bevy 会转换到下一个状态。
同一帧更新中可以做多次状态转换,Bevy 会处理所有这些转换,在进入下一个[状态]之前执行所有相关的系统。
状态栈
除了从一个状态完全转换到另一个状态,还可以覆盖状态,形成堆栈。
状态栈能实现“游戏暂停”屏幕或覆盖菜单等功能,而游戏世界仍在后台可见/运行。
可以让某些系统在状态“不活动”时(也就是在后台,其他状态在上面运行)仍在运行。也可以添加一次性系统,在“暂停”或“恢复”状态时运行。[TODO:需要实践一下“一次性系统”]
在 [App Builder] 中:
#![allow(unused)] fn main() { // 一直运行 .add_system_set( SystemSet::on_update(AppState::InGame) .with_system(playe_movement) ) // TODO:player idle animation while paused .add_system_set( SystemSet::on_inactive_update(AppState::InGame) .with_system(player_idle) ) // TODO:animations both while paused and while active .add_system_set( SystemSet::on_in_stack_update(AppState::InGame) .with_system(animate_trees) .with_system(animate_water) ) // 在这个状态“暂停”时运行 .add_system_set( SystemSet::on_pause(AppState::InGame) .with_system(hide_enemies) ) // 在这个状态“恢复”时运行 .add_system_set( SystemSet::on_resume(AppState::InGame) .with_system(reset_player) ) // 在这个状态第一次“进入”时运行 .add_system_set( SystemSet::on_enter(AppState::InGame) .with_system(setup_player) .with_system(setup_map) ) // 在这个状态“离开”时运行 .add_system_set( SystemSet::on_exit(AppState::InGame) .with_system(despawn_player) .with_system(despawn_map) ) }
用 push 或 pop 来管理状态:
#![allow(unused)] fn main() { // 推入 Paused 状态 app_state.push(AppState::Paused).unwrap(); // 弹出当前状态(Paused 状态),回到之前的状态 app_state.pop().unwrap(); }
(如前所示,用 .set 替换栈顶的活动状态)
已知的陷阱和限制
与其他运行标准结合
因为 State 是用 RunCriteria 来实现的,所以不能与 RunCriteria(例如用到 [Fixed Timestep])结合使用。
如果在使用了 State 的 SystemSet 上添加一个 RunCriteria,就会取代 State,表现为 State 无效,RunCriteria 生效。
使用一些技巧仍然可以做到把状态和运行标准结合起来使用。(TODO) show an example of how it could be done.
可以考虑 iyes_loopless,它没有此类限制。
Multiple Stages(TODO)
If you need state-dependent systems in multiple stages, a workaround is required.
You must add the state to one stage only, and then call .get_driver() and add that to the other stages before any state-dependent system sets in those stages.
可以考虑 iyes_loopless,它没有此类限制。
附带输入
如果要用 Input<T>(按钮/键盘)触发状态转换,要调用 .reset手动清除输入:
#![allow(unused)] fn main() { fn esc_to_menu( mut keys: ResMut<Input<KeyCode>>, mut app_state: ResMut<State<AppState>>, ) { if key.just_pressed(KeyCode::Escape) { app_state.set(AppState::MainMenu).unwrap(); keys.reset(KeyCode::Escape); } } }
(注意,要求使用 ResMut)
不用 .reset 会引起问题(目前是 issue),查看 triggering state transition from player input locks the game。
iyes_loopless没有此 issue。
事件
如果某个系统不是每帧运行,那么在暂停状态期间,系统会错过所有事件。避免这个陷阱的方法是实现 [Manual Event Clearing],手动管理相关事件类型的生命周期。
Latest commit 1d3f6e4
运行标准
运行标准是一种机制,控制 Bevy 在运行时运行特定系统。能够制作仅在特定条件下运行的功能。
运行标准是低级原语。Bevy在上层提供了高级抽象,比如[状态]。如果真的需要更直接的控制,可以用没有高级抽象的运行标准。
运行标准可以应用于各个系统、系统集和阶段。
运行标准是 Bevy 的系统,它返回一个类型为enum ShouldRun的值。运行标准就像像普通系统一样,接受任何系统参数。
这个例子展示了如何使用运行标准来实现不同的多人模式:
use bevy::ecs::schedule::ShouldRun; #[derive(Debug, PartialEq, Eq)] enum MultiplayerKind { Client, Host, Local, } fn run_if_connected( mode: Res<Multiplayerkind>, session: Res<MyNetworkSession>, ) -> ShouldRun { if *mode == MultiplayerKind::Client && session.is_connected() { ShouldRun::Yes } else { ShouldRun::No } } fn run_if_host( mode: Res<MultiplayerKind>, ) -> ShouldRun { if *mode == MultiplayerKind::Host || *mode == MultiplayerKind::Local { ShouldRun::Yes } else { ShouldRun::No } } fn main() { App::new() .add_plugins(DefaultPlugins) // 如果我们当前连接到服务器,激活客户端系统 .add_system_set( SystemSet::new() .with_run_criteria(run_if_connected) .before("input") .with_system(server_session) .with_system(fetch_server_updates) ) // 如果我们是游戏主机,激活游戏主机系统 .add_system_set( SystemSet::new() .with_run_criteria(run_if_host) .before("input") .with_system(host_session) .with_system(host_player_movement) .with_system(host_enemy_ai) ) // 其他系统 .add_system(smoke_particles) .add_system(water_animation) .add_system_set( SystemSet::new() .label("input") .with_system(keyboard_input) .with_system(gamepad_input) ) .run(); }
已知的陷阱
多个运行标准
无法建立以多个运行条件为条件的系统。Bevy 有一个 .pipe 方法来“链接”运行条件,这可以能修改运行标准的输出,但这在实践中非常有限。
考虑使用 iyes_loopless,它允许你使用任意数量的运行条件来控制系统,并且不会阻止你使用状态或固定的时间间隔。
事件
如果某个接收事件的系统不是每帧运行,那么系统不运行时,会错过所有事件。避免这个陷阱的方法是实现 [Manual Event Clearing],手动管理相关事件类型的生命周期。
Latest commit 85dac0c
标签
标签可以标记应用程序中的各种事物,例如 [System]、[Run Criteria]、[Stage]、[Ambiguity Set]。
标签常用于[系统排序]和添加[阶段]。
Bevy 巧用 Rust 类型系统实现了可用字符串或自定义类型来制作标签,甚至可以混合起来使用。
todo:未测试,大意是字符串很快,但不方便编译器报错。 Using strings for labels is quick and easy for prototyping. However, they are easy to mistype and are unstructured. The compiler cannot validate them for you, to catch mistakes.
todo:未测试,意思是建议用 enum 来定义标签。 You can also use custom types (usually enums) to define your labels. This allows the compiler to check them, and helps you stay organized in larger projects.
根据需要来派生相应的特性:StageLabel、SystemLabel、RunCriteriaLabel、AmbiguitySetLabel。
任何类型的值都可以作为标签,只要这个类型具有以下的 Rust 特型:
Clone + PartialEq + Eq + Hash + Debug(和隐含的 + Send + Sync + 'static)。
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(SystemLabel)] enum MySystems { InputSet, Movement, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(SystemLabel)] enum MyStages { Prepare, Cleanup, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(SystemLabel)] strut DebugStage; fn main() { App::new() .add_plugins(DefaultPlugins) // 添加游戏系统 .add_system_set( SystemSet::new() .label(MySystems::InputSet) .with_system(keyboard_input) .with_system(gamepad_input) ) .add_system(player_movement.label(MySystems::Movement)) // 临时调试系统,仅用一个字符串标签 .add_system(debug_movement.label("temp-debug")) // 添加自定义阶段:注意,Bevy 的 CoreStage 也是枚举! .add_stage_before(CoreStage::Update, MyStages::prepare, SystemStage::parallel()) .add_stage_after(CoreStage::Update, MyStages::Cleanup, SystemStage::parallel()) .add_stage_after(CoreStage::Update, DebugStage, SystemStage::parallel()) // 这里只需使用一个字符串 .add_stage_before(CoreStage::PostUpdate, "temp-debug-hack", SystemStage::parallel()) .run(); }
对于快速的游戏原型开发,只需用字符串作为标签就可以了。但是,把标签定义为自定义类型,Rust 编译器可以检查它们,IDE 也能够可以自动完成。这是推荐的方式,因为自定义类型的标签可以防止错误,在大型项目中保持组织性。
Latest commit d7a897d
阶段
所有在 Bevy 中运行的系统都包含在阶段中。每更新一帧,Bevy 都会按顺序执行每个阶段。在每个阶段内,Bevy 的调度算法可以使用多个 CPU 核心并行运行多个系统,以获得良好的性能。
各阶段之间的边界实际上是硬同步点。同步点可以确保上一阶段的所有系统全部完成后,下一阶段的系统才能开始,并且在某一时刻没有任何系统运行。
这样才能应用 Commands(或者说能安全地应用 Commands)。系统用 Commands 的任何操作只会会在每个阶段结束时进行。
在内部,Bevy 至少有这些内置的阶段:
- main app(CoreStage):
First、PreUpdate、Update、PostUpdate、Last - sub-app(RenderStage):
Extract、Prepare、Quene、PhaseSort、Render、Cleanup
把系统添加进 App 时,默认被添加到 CoreStage::Update。
Bevy 的内部系统在其他阶段,与游戏逻辑分开,确保了自身的正确排序。
如果想把系统添加到 Bevy 的内部阶段,要提防与 Bevy 的内部系统发生潜在的意外交互。记住:Bevy 的内部系统是用普通系统和 ECS 实现的,就像你自己的写的系统一样!
可以添加自定义阶段。例如,与调试有关的系统应该在游戏逻辑后面执行:
fn main() { // 用 debug 标签来表示 “DEBUG 阶段” static DEBUG: &str = "debug"; App::new() .add_plugins(DefaultPlugins) // 在 Bevy 的 Update 节点后面添加 “DEBUG 阶段”,并使 DEBUG 成为单线程 .add_stage_after(CoreStage::Update, DEBUG, SystemStage::singel_threaded()) // 这些系统默认添加到 `CoreStage::Update` .add_system(player_gather_xp) .add_system(player_take_damage) // 这些系统添加到 “DEBUG 阶段” .add_system_to_stage(DEBUG, debug_player_hp) .add_system_to_stage(DEBUG, debug_stats_change) .add_system_to_stage(DEBUG, debug_new_hostiles) .run(); }
如果要管理系统相对于彼此的先后运行顺序,最好不要用阶段,而要用显式系统排序。阶段限制了游戏的并行执行和性能。
但是,阶段能方便地确认所有系统是否已经全部完成,阶段也是应用 Commands 的唯一方式。
todo:未测试,不懂。 If you have systems that need to rely on the actions that other systems have performed by using Commands, and need to do so during the same frame, placing those systems into separate stages is the only way to accomplish that.
Latest commit d7a897d
Removal Detection
相关的官方文档:[removal_detection]
删除检测不同于变化检测。如果删除了数据,ECS 就不再拥有这些数据,也就不能继续追踪它们了。
但是删除检测对于某些应用程序很重要,因此 Bevy 提供了有限的形式。
组件
todo:看得不明不白、未测试,等 Bevy 改进吧。
#[derive(Component)] struct Seen; fn main() { App::new() .add_plugins(DefaultPlugins) // 把系统添加到 PreUpdate 阶段 .add_system_to_stage(CoreStage::PreUpdate, remove_components) // 用来检测的系统会在 PreUpdate 之后的阶段(Update)运行 .add_system(detect_removals) .run(); } fn removal_components( mut commands: Commands, q: Query<(Entity, &Transform), With<Seen>>, ) { for (e, transform) in q.iter() { if transform.translation.y < -10.0 { // 删除 Seen 组件 commands .entity(e) .remove::<Seen>(); } } } fn detect_removals ( removals: RemovedComponents<Seen>, // ...(todo:不懂)... ) { for entity in removals.iter() { // 做一些事 // 可以用 Commands::entity() 或 Query::get() } }
资源
略。Bevy 不提供?那就不看了。
Latest commit a926cd8
ParamSet
出于安全原因,系统的参数不能出现数据竞争。例如,下面的例子会报错:
#![allow(unused)] fn main() { fn test( transforms_a: Query<&Transform, With<ComponentA>>, mut transforms_b: Query<&mut Transform, With<ComponentB>>, ) { // 就跟 Rust 的引用、借用规则一样 // 一个解决办法就是把 // Query<&mut Transform, With<ComponentB>> // 改成 // Query<&mut Transform, (With<ComponentB>, Without<ComponentA>)> } }
Bevy 提供的解决方案是 ParamSet:
#![allow(unused)] fn main() { fn 重设生命值( // 同时查询“敌人”和“玩家”的 `Health` 组件 mut set: ParamSet<( Query<&mut Health, With<Enemy>>, Query<&mut Health, With<Player>>, &World, )>, ) { // 设置“敌人”的生命值(用第一个参数 p0) for mut health in set.p0().iter_mut() { health.hp = 50.0; } // 设置“玩家”的生命值(用第二个参数 p1) for mut health in set.p1().iter_mut() { health.hp = 100.0; } // 从游戏世界获取一些数据(用第三个参数 p2) let my_resource = set.p2().resource::<MyResource>(); // 没有数据竞争! } }
最多可以有 8 个参数(p0、p1、p2、p3、p4、p5、p6、p7)。
Latest commit d7a897d
系统链接
相关的官方文档:[system_chaining]
多个 Rust 函数可以组成一个 Bevy 系统。
定义多个有输入有输出的函数,把它们连接在一起,当成一个系统来运行。
这就是“系统链接”。请注意区别:不是创建“多个系统”以链的顺序执行,而是创建“一个系统”执行“多个 Rust 函数”。
系统链接不是用于系统之间的通信,请用用事件来通信。
好的应用程序能够从系统中返回错误(允许使用 Rust 的 ? 操作符)并在合适的地方处理错误:
#![allow(unused)] fn main() { fn net_receive(mut netcode: ResMut<MyNetProto>) -> std::io::Result<()> { netcode.receive_update()?; Ok(); } fn handle_io_errors(In(result): In<std::io::Result<()>>) { if let Err(e) = result { eprintln!("I/O error occurred: {}", e); } } }
上面的系统不能单独注册(Bevy 不知道怎样这些系统的处理输入和输出),必须组合成系统链:
fn main() { App::new() // ... .add_system(net_receive.chain(handle_io_errors)) // ... .run(); }
性能警告
Bevy 把整个链条当成一个大的系统,所有的资源和查询都组合在一起,这意味着并行性可能受到限制,影响性能。
todo:这句不理解,影响链内还是另一条链? 不要把需要可变访问的系统添加进链条,否则会阻止所有受影响的链的并行运行。
Latest commit a530933
直接访问世界
World 是 Bevy ECS 存储所有数据和相关元数据的地方。世界保持对资源、实体和组件的追踪。
世界(World) -> 调度(Schedule) -> 阶段(Stage) -> 系统(System)。
通常情况下,系统的参数会限制系统能够从世界访问的数据,要操作世界,只能使用 Commands。
然而,还是有几种方法能够直接访问世界,做到完全而自由地处理数据:
todo:未测试
- Exclusive systems
- FromWorld impls
- Via the App builder
- Manually created Worlds for purposes like tests or scenes
- Custom Commands
- Custom Stage impls (not recommended, prefer exclusive systems)
todo:很多 todo,只能 todo 了。
Latest commit 2d4b9c6
独占系统
Latest commit 00bfc62
Show Framerate in Console
用 Bevy 内置的诊断系统在控制台输出帧数:
use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin} fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugin(LogDiagnosticsPlugin::default()) .add_plugin(FrameTimeDiagnosticsPlugin::default()) .run(); }
Latest commit 1fa65ad
鼠标指针坐标转换成世界坐标
Bevy 目前无法通过内置函数直接获得鼠标指针所在的全局位置。
3D 游戏
第三方插件:bevy_mod_picking
2D 游戏
todo:等更新。
Latest commit d7a897d
Browser(WebAssembly)
介绍
针对WebAssembly(WASM)进行编译,允许游戏嵌入到网页中并在浏览器中运行。
但是性能受到限制,并且不支持多线程,还要注意第三方插件与WASM的兼容性。
快速入门
rustup target install wasm32-unknown-unknown
现在可以在浏览器运行bevy项目了。
wasm-server-runner
下载安装:
cargo install wasm-server-runner
设置项目文件.cargo/config.toml:
[target.wasm32-unknown-unknown]
runner = "wasm-server-runner"
运行:
cargo run --target wasm32-unknown-unknown
wasm-bindgen
把游戏部署到网站,可以用wasm-bindgen生成所需的所有文件,运行:
cargo build --release --target wasm32-unknown-unknown
wasm-bindgen --out-dir ./out/ --target web ./target/
可以在./out/目录找到所需文件,然后放在网页服务器上。
高级工具
- Trunk
- wasm-pack