Bevy 0.3

于 2020 年 11 月 3 日发布,作者 Carter Anderson ( 一只戴着猫耳并挥舞触手的卡通人物,Octocat:GitHub 的吉祥物和标识 @cart 一只灰色飞鸟的矢量图;X(前身为 Twitter)的旧标识 @cart_cart 指向右边的三角形,在一个圆角矩形中;YouTube 的标识 cartdev )

在发布 Bevy 0.2 之后的一个多月里,感谢 **59** 位贡献者、**122** 个拉取请求以及我们 慷慨的赞助者,我很高兴宣布 **Bevy 0.3** 版本已在 crates.io 上发布!

对于那些不了解的人来说,Bevy 是一款使用 Rust 构建的,令人耳目一新地简单的数据驱动游戏引擎。您可以查看 快速入门指南 以开始使用。Bevy 也是永远免费且开源的!您可以在 GitHub 上获取完整的 源代码

以下是本版本中的一些亮点

初始 Android 支持 #

作者:@enfipy,@PrototypeNM1,@endragor,@naithar

您可以通过以下步骤尝试 Bevy Android 示例此处说明。虽然很多功能都已实现,但请注意,此版本还处于非常早期的阶段。有些功能可能会正常工作,而另一些则可能无法正常工作。现在是深入了解并帮助我们填补空白的好时机!

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 支持 #

作者:@simlay,@MichaelHills,@Dash-L,@naithar

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(并移植到新的 AssetIo,作者 @cart)

@mrk-its 一直致力于扩展 Bevy 的 WASM 支持。在本版本中,我们实现了 WASM 资产加载。您现在可以在发布到 WASM 时加载资产,就像在任何其他平台上一样。

asset_server.load("sprite.png");

如果资产尚未加载,这将发出 fetch() 请求以通过 HTTP 检索资产。

@mrk-its 还一直在构建一个自定义 WebGL2 bevy_render 后端。它已经相当可用了,但还没有完全准备好。敬请期待更多相关新闻!

触控输入 #

作者:@naithar

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> 资源来使用原始触控事件。

资产系统改进 #

作者:@cart

资产句柄引用计数 #

当资产的“句柄引用计数”达到零时,现在会自动释放资产。这意味着您不再需要手动释放资产。

// 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 场景加载器 #

作者:@cart

到目前为止,GLTF 加载器非常有限。它只能加载 GLTF 文件中的第一个带有单个纹理的网格。对于 **Bevy 0.3**,我们利用资产系统改进编写了一个新的 GltfLoader,该加载器将 GLTF 文件加载为 Bevy 场景,以及文件中的所有网格和纹理。

以下是 Bevy 加载 Khronos Flight Helmet 示例,它包含多个网格和纹理!

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 改进 #

作者:@cart

查询人体工程学 #

在本版本中,我终于能够删除我在 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 次,而不是指单个组件查找。

getting an entity's component

这是最大的胜利。通过从查询系统中移除锁和安全检查,我们能够显著降低从系统内部检索特定实体组件的成本。

我将与 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 结果(省略了没有明显变化的基准测试)。

组件插入(以微秒为单位,越小越好)#

component insertion

组件添加/移除(以毫秒为单位,越小越好)#

component add/remove

碎片化迭代(以纳秒为单位,越小越好)#

fragmented iteration

线程本地资源#

某些资源类型无法(或不应该)在线程之间传递。这对于像窗口、输入和音频这样的底层 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)。现在,可以“只读”的查询通过不可变借用检索其结果。

网格改进#

灵活的网格顶点属性#

作者:@julhe

Bevy 网格以前需要正好三个“顶点属性”:positionnormaluv。这对于大多数情况都有效,但有一些情况需要其他属性,例如“顶点颜色”或“动画骨骼权重”。Bevy 0.3 添加了对自定义顶点属性的支持。网格可以定义它们想要的任何属性,而着色器可以消耗它们想要的任何属性!

这是一个示例,说明了如何定义一个消耗具有附加“顶点颜色”属性的网格的自定义着色器。

custom_vertex_attribute

索引缓冲区专业化#

作者:@termhn

渲染网格通常涉及使用顶点“索引”来减少重复顶点信息。Bevy 以前将这些索引的精度硬编码为 u16,对于某些情况来说太小了。现在渲染管道可以根据配置的索引缓冲区进行“专业化”,该缓冲区现在默认为 u32,以涵盖大多数用例。

变换重写#

作者:@MarekLg(在 @AThilenius、@bitshifter、@termhn 和 @cart 的一些设计帮助下)

变换很重要,需要正确处理。它们在引擎的许多切片中使用,用户代码不断接触它们,并且计算起来相对昂贵:尤其是变换层次结构。

在上一版本中,我们极大地简化了 Bevy 的变换系统,使用一个整合的 TransformGlobalTransform 代替多个独立的 TranslationRotationScale 组件(这些组件与 TransformGlobalTransform 同步)。这使得面向用户的 API/数据流更简单,底层实现也更简单。Transform 组件由一个 4x4 矩阵支持。我按下了巨大的绿色“合并”按钮……很高兴我们终于解决了变换问题!

事实证明,还需要做更多工作!@AThilenius 指出,使用 4x4 矩阵作为仿射变换的真实来源会导致随着时间的推移累积误差。此外,变换 API 使用起来仍然有点麻烦。在 @termhn 的建议下,我们决定研究使用“相似性”作为真实来源。这带来了以下好处。

  1. 不再累积误差。
  2. 我们可以直接公开平移/旋转/缩放字段,这简化了 API。
  3. 在某些情况下,存储和计算层次结构更便宜。

我们共同决定这是一条前进的良好道路,现在我们有一个重写版本,它甚至更好。是的,这是一个另一个重大更改,但这就是我们将 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;
}

与上一版本相比,它更易于使用、更正确,并且应该也稍快一些。

游戏手柄设置#

作者:@simpuid

新添加的 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()
        },
    );
}

插件组#

作者:@cart

如果您使用过 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);

动态窗口设置#

作者:@mockersf

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
    ));
}

文档可搜索性#

作者:@memoryruins

bevy crate 文档搜索功能现在会返回所有子 crate(如 bevy_sprite)的结果。由于如何为重新导出的 crate 生成文档,默认情况下 bevy 搜索索引只涵盖“prelude”。@memoryruins 找到了解决这个问题的方法,方法是在这些模块中创建新的模块,并将每个 crate 的内容导出到这些模块中(而不是为 crate 创建别名)。

docs

更改日志#

已添加#

已更改#

已修复 #

贡献者 #

衷心感谢 **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