Bevy 0.3
于 2020 年 11 月 3 日发布,作者 Carter Anderson (
@cart
@cart_cart
cartdev )
在发布 Bevy 0.2 之后的一个多月里,感谢 **59** 位贡献者、**122** 个拉取请求以及我们 慷慨的赞助者,我很高兴宣布 **Bevy 0.3** 版本已在 crates.io 上发布!
对于那些不了解的人来说,Bevy 是一款使用 Rust 构建的,令人耳目一新地简单的数据驱动游戏引擎。您可以查看 快速入门指南 以开始使用。Bevy 也是永远免费且开源的!您可以在 GitHub 上获取完整的 源代码。
以下是本版本中的一些亮点
初始 Android 支持 #
您可以通过以下步骤尝试 Bevy Android 示例:此处说明。虽然很多功能都已实现,但请注意,此版本还处于非常早期的阶段。有些功能可能会正常工作,而另一些则可能无法正常工作。现在是深入了解并帮助我们填补空白的好时机!
这是一项跨越多个项目的巨大团队合作努力
- Bevy:重写了 bevy-glsl-to-spirv 以支持 android / 静态库 (@PrototypeNM1,@enfipy)
- Bevy:使用 Android Asset Manager 的
bevy_asset
后端 (@enfipy) - Bevy:触摸支持 (@naithar)
- Bevy:纹理格式修复 (@enfipy)
- Bevy:UI 触摸修复,触摸力,以及 android 示例 (@enfipy)
- Cpal:android 音频支持 (@endragor)
- android-ndk-rs / cargo-apk:修复以支持 Bevy 项目结构 (@PrototypeNM1)
初始 iOS 支持 #
Bevy 现在可以在 iOS 上运行!

您可以通过以下步骤尝试 Bevy iOS 示例:此处说明。此版本也处于早期阶段:有些功能可能会正常工作,而另一些则可能无法正常工作。
这也是一项跨越多个项目的巨大团队合作努力
- Bevy:XCode 项目 / 示例 (@simlay,在 @MichaelHills 的帮助下)
- Bevy:使用 shaderc 的运行时着色器编译 (@MichaelHills)
- Bevy:Rodio 升级 (@Dash-L)
- Bevy:触摸支持 (@naithar)
- Winit:修复 iOS 横屏视图 (@MichaelHills)
- RustAudio:iOS 支持 (@simlay 和 @MichaelHills)
已知问题
WASM 资产加载 #
@mrk-its 一直致力于扩展 Bevy 的 WASM 支持。在本版本中,我们实现了 WASM 资产加载。您现在可以在发布到 WASM 时加载资产,就像在任何其他平台上一样。
asset_server.load("sprite.png");
如果资产尚未加载,这将发出 fetch()
请求以通过 HTTP 检索资产。
@mrk-its 还一直在构建一个自定义 WebGL2 bevy_render
后端。它已经相当可用了,但还没有完全准备好。敬请期待更多相关新闻!
触控输入 #
Bevy 现在支持触控。
fn touch_system(touches: Res<Touches>) {
// you can iterate all current touches and retrieve their state like this:
for touch in touches.iter() {
println!("active touch: {:?}", touch);
}
for touch in touches.iter_just_pressed() {
println!("just pressed {:?}", touch);
}
for touch in touches.iter_just_released() {
println!("just released {:?}", touch);
}
for touch in touches.iter_just_cancelled() {
println!("just cancelled {:?}", touch);
}
}
您还可以使用 Events<TouchInput>
资源来使用原始触控事件。
资产系统改进 #
资产句柄引用计数 #
当资产的“句柄引用计数”达到零时,现在会自动释放资产。这意味着您不再需要手动释放资产。
// Calling load() now returns a strong handle:
let handle = asset_server.load("sprite.png");
// Note that you no longer need to unwrap() loaded handles. Ergonomics for the win!
// Cloning a handle increases the reference count by one
let second_handle = handle.clone();
// Spawn a sprite and give it our handle
commands.spawn(SpriteComponents {
material: materials.add(handle.into()),
..Default::default()
});
// Later in some other system:
commands.despawn(sprite_entity);
// There are no more active handles to "sprite.png", so it will be freed before the next update
资产加载器现在可以加载多个资产 #
在以前的版本中,AssetLoaders
只能生成单个类型的单个资产。在 **Bevy 0.3** 中,它们现在可以为任何类型生成任意数量的资产。当加载像 GLTF 文件这样的资产时,旧的行为非常有限,这些文件可能会生成许多网格、纹理和场景。
子资产加载 #
有时您只想从资产源加载特定资产。您现在可以像这样加载子资产
// Mesh0/Primitive0 references the first mesh primitive in "my_scene.gltf"
let mesh = asset_server.load("my_scene.gltf#Mesh0/Primitive0");
AssetIo 特性 #
AssetServer
现在由 AssetIo
特性支持。这使我们能够从我们想要的任何存储位置加载资产。这意味着在桌面版中,我们现在从文件系统加载,在 Android 版中,我们使用 Android Asset Manager,在 Web 版中,我们使用 fetch()
API 发出 HTTP 请求。
资产依赖项 #
资产现在可以依赖于其他资产,这些资产将在加载原始资产时自动加载。这在加载诸如“场景”之类的资产时很有用,这些资产可能引用其他资产源。我们在新的 GLTF 加载器中利用了这一点。
已删除 AssetServer::load_sync() #
这可能会引起一些争议,但 AssetServer::load_sync()
必须被移除!此 API 对 WASM 不友好,鼓励用户为了方便而阻塞游戏执行(这会导致“卡顿”),并且与新的 AssetLoader API 不兼容。资产加载现在始终是异步的。load_sync()
的用户应该改为 load()
他们的资产,在他们的系统中检查加载状态,并相应地更改游戏状态。
GLTF 场景加载器 #
到目前为止,GLTF 加载器非常有限。它只能加载 GLTF 文件中的第一个带有单个纹理的网格。对于 **Bevy 0.3**,我们利用资产系统改进编写了一个新的 GltfLoader
,该加载器将 GLTF 文件加载为 Bevy 场景
,以及文件中的所有网格和纹理。
以下是 Bevy 加载 Khronos Flight Helmet 示例,它包含多个网格和纹理!
以下是加载 GLTF 文件并将其作为场景生成的系统的完整代码
fn load_gltf_system(mut commands: Commands, asset_server: Res<AssetServer>) {
let scene_handle = asset_server.load("models/FlightHelmet/FlightHelmet.gltf");
commands.spawn_scene(scene_handle);
}
Bevy ECS 改进 #
查询人体工程学 #
在本版本中,我终于能够删除我在 Bevy ECS 中真正厌恶的一件事。在 Bevy 的先前版本中,遍历 Query
中的组件看起来像这样
for (a, b) in &mut query.iter() {
// The `&mut` here just felt so unnatural
}
// Or if you preferred you could do this
for (a, b) in query.iter().iter() {
// query.iter().iter()? Really???
}
类似地,检索特定实体组件看起来像这样
if let Ok(mut result) = query.entity(entity) {
if let Some((a, b)) = result.get() {
// access components here
}
}
在 **Bevy 0.3** 中,您只需执行以下操作
// iteration
for (a, b) in query.iter() {
// sweet ergonomic bliss
}
// entity lookup
if let Ok((a,b)) = query.get(entity) {
// boilerplate be gone!
}
您可能会自然地想到类似的东西
为什么花了这么长时间?为什么删除一个 &mut
会很困难?
这是一个很长的故事!简而言之
- 旧的 API 出现这种方式是有原因的。它是良好设计选择的结果,这些选择可以在并行环境中防止不安全的内存访问。
query.iter()
实际上并没有返回迭代器。它返回一个包装器,该包装器在组件存储上持有原子锁。query.entity()
返回的类型也是如此。- 删除这些“包装器类型”将允许不安全的行为,因为另一个 Query 可以以违反 Rust 可变性规则的方式访问相同的组件。
- 由于迭代器实现和 rust 编译器中的怪癖,删除包装器类型破坏了迭代性能,大约下降了 ~2-3 倍。
幸运的是,我们终于找到了解决所有这些问题的方法。新添加的 QuerySets
使我们能够完全删除锁(以及包装器类型)。通过完全重写 QueryIter
,我们能够避免删除包装器带来的性能损失。继续阅读以了解详情!
100% 无锁并行 ECS #
Bevy ECS 现在完全没有锁。在 Bevy 0.2 中,我们实现了对 World
的直接访问和“for-each”系统的无锁访问。这是可能的,因为 Bevy ECS 调度器确保系统只以符合 Rust 可变性规则的方式并行运行。
我们无法从 Query
系统中删除锁,因为有以下这样的系统
fn conflicting_query_system(mut q0: Query<&mut A>, mut q1: Query<(&mut A, &B)>) {
let a = q0.get_mut(some_entity).unwrap();
let (another_a, b) = q1.get_mut(some_entity).unwrap();
// Aaah!!! We have two mutable references to some_entity's A component!
// Very unsafe!
}
锁确保第二个 q1.get_mut(some_entity)
访问出现恐慌,从而确保我们安全无虞。在 **Bevy 0.3** 中,类似于 conflicting_query_system
这样的系统将在构建调度器时失败。默认情况下,系统不能有冲突的查询。
但是,在某些情况下,系统需要有冲突的查询才能完成其工作。对于这些情况,我们添加了 QuerySets
fn system(mut queries: QuerySet<(Query<&mut A>, Query<(&mut A, &B)>)>) {
for a in queries.q0_mut().iter_mut() {
}
for (a, b) in queries.q1_mut().iter_mut() {
}
}
通过将我们冲突的 Queries
放在 QuerySet
中,Rust 借用检查器可以保护我们免受不安全的查询访问。
因此,我们能够从 query.iter()
和 query.get(entity)
中删除所有安全检查,这意味着这些方法现在与它们的 World
对应方法(我们在 Bevy 0.2 中实现了无锁访问)完全相同。
性能改进 #
Bevy 在本版本中进行了一些性能改进
- 从查询访问中删除了原子锁,使 Bevy ECS 100% 无锁访问。
- 从查询访问中删除了原型“安全检查”。在这一点上,我们已经验证了给定的查询访问是安全的,因此我们不需要在每次调用时都再次检查。
QueryIter
进行了简化,使其更易于控制优化,从而可以移除迭代器包装器而不会影响性能。这还解决了一些性能不一致的问题,其中某些系统排列执行最佳,而其他系统则没有。现在一切都处于“快速路径”上!- 移植了来自上游 hecs 的一些性能改进,这改进了对高度碎片化的原型进行迭代,并提高了组件插入时间。
获取实体的组件(每 10 万次,以毫秒为单位,越小越好)#
注意:这些数字是指获取一个组件 100,000 次,而不是指单个组件查找。
这是最大的胜利。通过从查询系统中移除锁和安全检查,我们能够显著降低从系统内部检索特定实体组件的成本。
我将与 Legion ECS(另一个具有并行调度程序的优秀原型 ECS)进行了比较,以说明 Bevy 的新方法为何如此酷。Legion 在其系统中公开了一个直接的“世界状”API(称为 SubWorld)。SubWorld 的入口 API 无法提前知道将传入它的类型,这意味着它必须进行(相对)昂贵的安全检查,以确保用户不会请求访问不应该访问的内容。
Bevy 的调度程序会在提前对 Queries
进行预检查,这使得系统可以访问其结果,而无需任何额外的检查。
测试是在每次系统迭代中对特定实体的组件进行 100,000 次查找(并修改)。以下是这些测试在每种情况下如何执行的简要概述。
- bevy(世界):使用
world.get_mut::<A>(entity)
进行直接World
访问 - bevy(系统):包含
Query<&mut A>
的系统,它使用query.get_mut(entity)
访问组件。 - legion(世界):使用
let entry = world.entry(entity); entry.get_component_mut::<A>()
进行直接World
访问 - legion(系统):使用
SubWorld
访问的系统,使用let entry = world.entry(entity); entry.get_component_mut::<A>()
值得注意的是,使用 query.get_component::<T>(entity)
而不是 query.get(entity)
确实需要安全检查,原因与 legion 入口 API 相同。我们无法提前知道调用者将传入该方法的组件类型,这意味着我们必须检查它以确保它与 Query
匹配。
此外,以下是一些相关的 ecs_bench_suite 结果(省略了没有明显变化的基准测试)。
组件插入(以微秒为单位,越小越好)#
组件添加/移除(以毫秒为单位,越小越好)#
碎片化迭代(以纳秒为单位,越小越好)#
线程本地资源#
某些资源类型无法(或不应该)在线程之间传递。这对于像窗口、输入和音频这样的底层 API 来说通常是正确的。现在可以将“线程本地资源”添加到 Resources
集合中,这些资源只能使用“线程本地系统”从主线程访问。
// in your app setup
app.add_thread_local_resource(MyResource);
// a thread local system
fn system(world: &mut World, resources: &mut Resources) {
let my_resource = resources.get_thread_local::<MyResource>().unwrap();
}
查询 API 更改#
首先,为了提高清晰度,我们重命名了 query.get::<Component>(entity)
为 query.get_component::<Component>(entity)
。我们现在使用 query.get(entity)
返回特定实体的“完整”查询结果。
为了允许对查询进行多次并发读取(在安全的情况下),我们添加了单独的 query.iter()
和 query.iter_mut()
API,以及 query.get(entity)
和 query.get_mut(entity)
。现在,可以“只读”的查询通过不可变借用检索其结果。
网格改进#
灵活的网格顶点属性#
Bevy 网格以前需要正好三个“顶点属性”:position
、normal
和 uv
。这对于大多数情况都有效,但有一些情况需要其他属性,例如“顶点颜色”或“动画骨骼权重”。Bevy 0.3 添加了对自定义顶点属性的支持。网格可以定义它们想要的任何属性,而着色器可以消耗它们想要的任何属性!
这是一个示例,说明了如何定义一个消耗具有附加“顶点颜色”属性的网格的自定义着色器。
索引缓冲区专业化#
渲染网格通常涉及使用顶点“索引”来减少重复顶点信息。Bevy 以前将这些索引的精度硬编码为 u16
,对于某些情况来说太小了。现在渲染管道可以根据配置的索引缓冲区进行“专业化”,该缓冲区现在默认为 u32
,以涵盖大多数用例。
变换重写#
变换很重要,需要正确处理。它们在引擎的许多切片中使用,用户代码不断接触它们,并且计算起来相对昂贵:尤其是变换层次结构。
在上一版本中,我们极大地简化了 Bevy 的变换系统,使用一个整合的 Transform
和 GlobalTransform
代替多个独立的 Translation
、Rotation
和 Scale
组件(这些组件与 Transform
和 GlobalTransform
同步)。这使得面向用户的 API/数据流更简单,底层实现也更简单。Transform
组件由一个 4x4 矩阵支持。我按下了巨大的绿色“合并”按钮……很高兴我们终于解决了变换问题!
事实证明,还需要做更多工作!@AThilenius 指出,使用 4x4 矩阵作为仿射变换的真实来源会导致随着时间的推移累积误差。此外,变换 API 使用起来仍然有点麻烦。在 @termhn 的建议下,我们决定研究使用“相似性”作为真实来源。这带来了以下好处。
- 不再累积误差。
- 我们可以直接公开平移/旋转/缩放字段,这简化了 API。
- 在某些情况下,存储和计算层次结构更便宜。
我们共同决定这是一条前进的良好道路,现在我们有一个重写版本,它甚至更好。是的,这是一个另一个重大更改,但这就是我们将 Bevy 标注为处于“实验阶段”的原因。现在是尽可能频繁地打破东西的时候,以确保我们找到能够经受住时间考验的良好 API。
这就是新的 Transform
API 在 Bevy ECS 系统中的样子。
fn system(mut transform: Mut<Transform>) {
// move along the positive x-axis
transform.translation += Vec3::new(1.0, 0.0, 0.0);
// rotate 180 degrees (pi) around the y-axis
transform.rotation *= Quat::from_rotation_y(PI);
// scale 2x
transform.scale *= 2.0;
}
与上一版本相比,它更易于使用、更正确,并且应该也稍快一些。
游戏手柄设置#
新添加的 GamepadSettings
资源使开发人员能够自定义每个控制器、每个轴/按钮的游戏手柄设置。
fn system(mut gamepad_settings: ResMut<GamepadSettings>) {
gamepad_settings.axis_settings.insert(
GamepadAxis(Gamepad(0), GamepadAxisType::LeftStickX),
AxisSettings {
positive_high: 0.8,
positive_low: 0.01,
..Default::default()
},
);
}
插件组#
如果您使用过 Bevy,您可能熟悉 App
初始化的这一部分。
app.add_default_plugins();
这添加了所有“核心”引擎功能(渲染、输入、音频、窗口等)的插件。它很简单,但也非常静态。如果您不想添加所有默认插件怎么办?如果您想创建自己的自定义插件集怎么办?
为了解决这个问题,我们添加了 PluginGroups
,它们是有序的插件集合,可以单独启用或禁用。
// This:
app.add_default_plugins()
// Has been replaced by this:
app.add_plugins(DefaultPlugins)
// You can disable specific plugins in a PluginGroup:
app.add_plugins_with(DefaultPlugins, |group| {
group.disable::<RenderPlugin>()
.disable::<AudioPlugin>()
});
// And you can create your own PluginGroups:
pub struct HelloWorldPlugins;
impl PluginGroup for HelloWorldPlugins {
fn build(&mut self, group: &mut PluginGroupBuilder) {
group.add(PrintHelloPlugin)
.add(PrintWorldPlugin);
}
}
app.add_plugins(HelloWorldPlugins);
动态窗口设置#
Bevy 提供了一个与后端无关的窗口 API。到目前为止,窗口设置只能在应用程序启动时设置一次。如果您想动态设置窗口设置,则必须直接与窗口后端(例如 winit)交互。
在此版本中,我们添加了使用 Bevy 窗口抽象在运行时动态设置窗口属性的功能。
// This system dynamically sets the window title to the number of seconds since startup. Because why not?
fn change_title(time: Res<Time>, mut windows: ResMut<Windows>) {
let window = windows.get_primary_mut().unwrap();
window.set_title(format!(
"Seconds since startup: {}", time.seconds_since_startup
));
}
文档可搜索性#
bevy
crate 文档搜索功能现在会返回所有子 crate(如 bevy_sprite)的结果。由于如何为重新导出的 crate 生成文档,默认情况下 bevy
搜索索引只涵盖“prelude”。@memoryruins 找到了解决这个问题的方法,方法是在这些模块中创建新的模块,并将每个 crate 的内容导出到这些模块中(而不是为 crate 创建别名)。
更改日志#
已添加#
- 触控输入
- iOS XCode 项目
- Android 示例,使用 bevy-glsl-to-spirv 0.2.0
- 引入鼠标捕获 API
bevy_input::touch
:实现触控输入- MacOS 上的方向键支持
- Android 文件系统支持
- app:PluginGroups 和 DefaultPlugins
PluginGroup
是一个插件集合,其中每个插件都可以启用或禁用。
- 支持使用
Axis<GamepadButton>
获取游戏手柄按钮/触发器值。 - 公开 Winit 装饰
- 启用在运行时更改窗口设置
- 公开一个 EventLoopProxy 指针来处理自定义消息
- 添加一种方法来指定纹理图集中的精灵之间的填充/边距
- 为捆绑包添加
bevy_ecs::Commands::remove
- 为
TextureFormat
实现Default
- 在 ChildBuilder 中公开 current_entity
AppBuilder::add_thread_local_resource
Commands::write_world_boxed
将一个预包装的世界写入器发送到 ECS 的命令队列。FrameTimeDiagnosticsPlugin
现在除了“帧时间”和“fps”之外,还显示“帧数”。- 添加层次结构示例
WgpuPowerOptions
用于在低功耗、高性能和自适应功耗之间选择。- 为更多类型派生
Debug
:#597、#632 - 索引缓冲区专业化
- 有关系统依赖项的更多说明
- 建议在 MacOS 上使用
-Zrun-dsymutil-no
进行更快的编译
已更改#
- ecs:符合人体工程学的 query.iter(),移除锁,添加 QuerySets
query.iter()
现在是一个真正的迭代器!QuerySet
允许处理冲突的查询,并在编译时进行检查。
- 重命名
query.entity()
和query.get()
query.get::<Component>(entity)
现在是query.get_component::<Component>(entity)
query.entity(entity)
现在是query.get(entity)
- 资产系统重构和 GLTF 场景加载
- 引入 WASM 实现的
AssetIo
- 将变换数据移出 Mat4
- 将游戏手柄状态代码与游戏手柄事件代码和其他自定义项分离
- gamepad:公开原始和已过滤的游戏手柄事件
- 在
wasm32
目标上不依赖于spirv-reflect
- 将动态插件加载移动到它自己的可选 crate 中
- 在
wasm32
目标上向WindowDescriptor
添加字段,以选择性地提供现有的画布元素作为 winit 窗口 - 调整
ArchetypeAccess
如何跟踪可变和不可变依赖项 - 尽可能在
Commands
和ChildBuilder
中使用FnOnce
- 运行器显式调用
App.initialize()
Color
的 sRGB 感知- 现在假定颜色是在非线性 sRGB 色彩空间中提供的。诸如
Color::rgb
和Color::rgba
之类的构造函数将被转换为线性 sRGB。 - 新的方法
Color::rgb_linear
和Color::rgba_linear
将接受已经在线性 sRGB 中的颜色(旧的行为) - 现在必须通过设置器和获取器访问单个颜色组件。
- 现在假定颜色是在非线性 sRGB 色彩空间中提供的。诸如
- 使用自定义顶点属性对
Mesh
进行全面检修- 现在可以通过
mesh.attributes.insert()
添加任何顶点属性。 - 请参阅
example/shader/mesh_custom_attribute.rs
- 已删除
VertexAttribute
、Vertex
、AsVertexBufferDescriptor
。 - 对于缺少的属性(着色器请求但网格未定义),Bevy 将提供一个零填充的备用缓冲区。
- 现在可以通过
- 多次销毁实体会导致发出调试级日志消息,而不是出现恐慌:#649,#651
- 迁移到 Rodio 0.12
- 新的音频播放方法可以在示例中找到。
- 添加了对为
Local<T>
系统资源插入自定义初始值的支持 #745
已修复 #
- 在设置动态绑定时正确更新绑定组 ID
- 在 AppExit 事件上正确退出应用程序
- 修复了针对不同 NaN 值,FloatOrd 哈希值不同的问题
- 修复了 QueryOne get 的添加行为
- 更新 camera_system 以修复后期添加摄像头的問題
- 注册
IndexFormat
作为属性 - 修复了 breakout 示例中的错误
- 通过合并父级更新系统来修复 PreviousParent 延迟
- 修复了启动时游戏手柄的连接事件错误
- 修复了波浪形文本
贡献者 #
衷心感谢 **59 位贡献者** 使此次发布(以及相关的文档)成为可能!
- alec-deason
- alexb910
- andrewhickman
- blunted2night
- Bobox214
- cart
- CGMossa
- CleanCut
- ColdIce1605
- Cupnfish
- Dash-L
- DJMcNab
- EllenNyan
- enfipy
- EthanYidong
- Gregoor
- HyperLightKitsune
- ian-h-chamberlain
- J-F-Liu
- Jerald
- jngbsn
- joshuajbouw
- julhe
- kedodrill
- lberrymage
- lee-orr
- liufuyang
- MarekLg
- Mautar55
- memoryruins
- mjhostet
- mockersf
- MrEmanuel
- mrk-its
- mtsr
- naithar
- navaati
- ndarilek
- nic96
- ocornoc
- Olaren15
- PrototypeNM1
- Ratysz
- Raymond26
- robertwayne
- simlay
- simpuid
- smokku
- stjepang
- SvenTS
- sY9sE33
- termhn
- tigregalis
- Vaelint
- W4RH4WK
- walterpie
- will-hart
- zgotsch
- Zooce