Bevy 0.10

发布日期:2023 年 3 月 6 日,作者:Bevy 贡献者

感谢 173 位贡献者、689 个拉取请求、社区审阅者以及我们慷慨的赞助商,我们很高兴宣布 Bevy 0.10 版本已发布在 crates.io 上!

对于那些不知道的人,Bevy 是一款使用 Rust 构建的简单而清爽的数据驱动游戏引擎。您可以查看我们的 快速入门指南 来立即体验它。它是免费且开源的,永远免费!您可以在 GitHub 上获取完整的 源代码。查看 Bevy 资源,了解社区开发的插件、游戏和学习资源的集合。

要将现有 Bevy 应用程序或插件更新到 Bevy 0.10,请查看我们的 0.9 到 0.10 迁移指南

自从我们几个月前发布最后一个版本以来,我们添加了大量新功能、错误修复和生活质量改进,但以下是一些亮点

  • ECS Schedule v3:Bevy 现在拥有更简单、更灵活的调度机制。系统现在存储在一个统一的调度中,命令可以通过 apply_system_buffers 明确应用,并且进行了许多生活质量改进和错误修复。
  • 级联阴影贴图:更优质的阴影贴图,可以覆盖更大的距离,贴图质量会随着相机而变化。
  • 环境贴图照明:基于 360 度环境图像的照明,可以廉价且大幅度地提高场景的视觉质量。
  • 深度和法线预渲染:在主渲染通道之前渲染场景的深度和法线纹理,可以实现新的效果(在某些情况下)提高性能。阴影贴图使用预渲染着色器,这使得透明纹理可以投射阴影。
  • 平滑骨骼动画过渡:平滑地过渡在同一时间播放的两个骨骼动画之间的动画!
  • 改进的 Android 支持:Bevy 现在可以在更多 Android 设备上开箱即用(有一些注意事项)。
  • 改进的 Bloom 效果:Bloom 效果现在看起来更好,更容易控制,视觉伪影也更少。
  • 距离和大气雾:使用 3D 距离和大气雾效果为场景添加深度和氛围!
  • StandardMaterial 混合模式:使用更多 PBR 材质混合模式实现各种有趣的效果。
  • 更多色调映射选择:为您的 HDR 场景选择 7 种流行的色调映射算法之一,以实现您想要的视觉风格。
  • 颜色分级:控制每个相机的曝光度、伽马值、“色调映射前饱和度”和“色调映射后饱和度”。
  • 并行流水线渲染:应用程序逻辑和渲染逻辑现在自动并行运行,从而显着提高性能。
  • 将窗口作为实体:窗口现在以实体而不是资源的形式表示,这改善了用户体验并解锁了新的场景。
  • 渲染器优化:我们在本轮开发中投入了大量精力来优化渲染器。Bevy 的渲染器比以往任何时候都快!
  • ECS 优化:同样,我们也对许多常见的 ECS 操作进行了加速。Bevy 应用程序获得了不错的速度提升!

ECS Schedule v3 #

作者:@alice-i-cecile、@maniwani、@WrongShoe、@cart、@jakobhellermann、@JoJoJet、@geieredgar 等等

感谢我们 ECS 团队的出色工作,备受期待的 "无阶段"调度 RFC 已实施!

Schedule v3 是大量设计和实施工作的结晶。调度 API 是 Bevy 开发体验的核心和决定性部分,因此我们在对 API 的下一个演变进行设计时,必须非常谨慎和细致。除了 RFC PR 之外,@maniwani 完成的初始实现 PR@alice-i-cecile 完成的 Bevy 引擎内部移植 PR 是非常好的起点,如果您想了解我们的流程和基本原理。众所周知,计划和实施是两码事。我们的最终实现与最初的 RFC 有些不同(以好的方式)。

发生了很多变化,但我们付出了很多努力来确保 迁移路径 对于现有应用程序来说比较简单。不要担心!

让我们来看看 0.10 中发布的内容!

一个统一的调度 #

您是否曾经想指定 system_asystem_b 之前运行,却遇到令人困惑的警告,指出 system_b 未找到,因为它位于不同的阶段?

不用再这样了!现在,单个 Schedule 中的所有系统都存储在一个单一数据结构中,并全局了解正在发生的事情。

这简化了我们的内部逻辑,使您的代码在重构时更加健壮,并允许插件作者指定高级不变性(例如,“移动必须在碰撞检测之前发生”),而不会将自己锁定到确切的调度位置。

main_schedule_diagram

这张使用 @jakobhellermann 的 bevy_mod_debugdump 箱子 制作的图表展示了 Bevy 默认调度的简化版本。

添加系统 #

系统(它只是 普通的 Rust 函数!)是您在 Bevy ECS 中定义游戏逻辑的方式。使用 Schedule v3,您可以像以前版本一样将系统添加到您的 App

app.add_system(gravity)

但是 Schedule v3 还有一些新招!您现在可以一次添加多个系统

app.add_systems((apply_acceleration, apply_velocity))

默认情况下,Bevy 会并行运行系统。在 Bevy 的早期版本中,您是这样排序系统的

app
    .add_system(walk.before(jump))
    .add_system(jump)
    .add_system(collide.after(jump))

您仍然可以这样做!但是您现在可以使用 add_systems 将其压缩

// much cleaner!
app.add_systems((
    walk.before(jump),
    jump,
    collide.after(jump),
))

before()after() 绝对是很有用的工具!但是,由于有了新的 chain() 函数,现在更容易按特定顺序运行系统

// This is equivalent to the previous example
app.add_systems((walk, jump, collide).chain())

chain() 将按定义顺序运行系统。链式调用还可以与每个系统的配置配对

app.add_systems((walk.after(input), jump, collide).chain())

可配置的系统集 #

Schedule v3 中,“系统集”的概念已重新定义,以支持更自然、更灵活地控制系统运行和调度的机制。旧的“系统标签”概念已与“集”概念合并,从而产生了一种简单而强大的抽象。

SystemSets 是系统的命名集合,它们在所有成员之间共享系统配置。相对于 SystemSet 对系统进行排序会将该排序应用于所有该集中的系统,以及每个系统的任何配置。

让我们直接看看它是什么样的。您是这样定义 SystemSets

#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
enum PhysicsSet {
    Movement,
    CollisionDetection,
}

您可以通过调用 in_set 方法将系统添加到集合中

app.add_system(gravity.in_set(PhysicsSet::Movement))

您可以将其与上面提到的新系统功能结合使用

app.add_systems(
    (apply_acceleration, apply_velocity)
        .chain()
        .in_set(PhysicsSet::Movement)
)

系统可以属于任意数量的集合

app.add_system(
    move_player
        .in_set(MoveSet::Player)
        .in_set(PhysicsSet::Movement)
)

配置是按如下方式添加到集合中的

app.configure_set(
    // Run systems in the Movement set before systems in the CollisionDetection set
    PhysicsSet::Movement.before(PhysicsSet::CollisionDetection)
)

集合可以嵌套在其他集合中,这将导致它们继承其父集合的配置

app.configure_set(MoveSet::Enemy.in_set(PhysicsSet::Movement))

集合可以多次配置

// In PlayerPlugin:
app.configure_set(MoveSet::Player.before(MoveSet::Enemy))

// In PlayerTeleportPlugin
app.configure_set(MoveSet::Player.after(PortalSet::Teleport))

至关重要的是,系统配置是严格累加的:您无法移除在其他地方添加的规则。这既是“反意大利面式”也是“插件隐私”方面的考虑。当该规则与 Rust 强大的类型隐私规则相结合时,插件作者可以仔细决定需要维护哪些确切的不变性,以及在不破坏使用者的前提下重新组织代码和系统。

配置规则必须彼此兼容:任何悖论(例如集合内的系统集、必须在集合之前和之后运行的系统、顺序循环等)都会导致运行时恐慌并显示有帮助的错误消息。

直接调度独占系统 #

“独占系统”是系统,它们可以直接访问整个 ECS 世界。因此,它们不能与其他系统并行运行。

自从 Bevy 诞生以来,Bevy 开发人员一直希望相对于普通系统来调度独占系统(并刷新命令)。

现在您可以做到!独占系统现在可以像任何其他系统一样进行调度和排序。

app
    .add_system(ordinary_system)
    // This works!
    .add_system(exclusive_system.after(ordinary_system))

这特别强大,因为命令刷新(应用在系统中添加的排队命令,用于执行诸如生成和销毁实体等操作)现在只是在 apply_system_buffers 独占系统中执行。

app.add_systems(
    (
        // This system produces some commands
        system_a,
        // This will apply the queued commands from system_a
        apply_system_buffers,
        // This system will have access to the results of
        // system_a's commands
        system_b,
    // This chain ensures the systems above run in the order
    // they are defined
    ).chain()
)

但是要小心这种模式:很容易很快地出现许多排序不佳的独占系统,从而导致瓶颈和混乱。

你会用如此强大的力量做什么?我们很想了解!

使用调度管理复杂控制流 #

但是,如果您想对您的调度做一些奇怪的事情呢?一些非线性的、分支的或循环的。你应该用什么?

事实证明,Bevy 已经拥有了这样一个很棒的工具:在独占系统内运行的调度。这个想法很简单

  1. 构建一个调度,存储您想要运行的任何复杂逻辑。
  2. 将该调度存储在资源中。
  3. 在独占系统中,执行您想要的任何任意 Rust 逻辑来决定您的调度是否以及如何运行。
  4. 暂时从世界中取出调度,在世界其他部分上运行它,以同时改变调度和世界,然后将它放回。

随着新的调度资源和 world.run_schedule() API 的加入,它比以往任何时候都更加✨ 人性化 ✨。

// A Schedule!
let mut my_schedule = Schedule::new();
schedule.add_system(my_system);

// A label for our new Schedule!
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
struct MySchedule;

// An exclusive system to run this schedule
fn run_my_schedule(world: &mut World) {
    while very_complex_logic() {
        world.run_schedule(MySchedule);
    }
}

// Behold the ergonomics!
app
    .add_schedule(MySchedule, my_schedule)
    .add_system(run_my_schedule);

Bevy 在Bevy 0.10中使用这种模式来处理五种截然不同的东西

  1. 启动系统:这些现在位于自己的调度中,该调度在应用程序启动时运行一次。
  2. 固定时间步长系统:另一个调度?!运行此调度的独占系统会累积时间,运行一个 while 循环,重复运行 CoreSchedule::FixedUpdate,直到所有累积的时间都已使用完。
  3. 进入和退出状态:大量的调度。每个运行逻辑以进入和退出状态变体的系统集合都存储在它自己的调度中,该调度基于 apply_state_transitions::<S> 独占系统中状态的变化而调用。
  4. 渲染:所有渲染逻辑都存储在它自己的调度中,以允许它相对于游戏逻辑异步运行。
  5. 控制最外层循环:为了处理“启动调度优先,然后是主调度”逻辑,我们将它全部包装在一个最小开销的 CoreSchedule::Outer 中,然后将我们的调度作为唯一的独占系统运行。

CoreSchedule开始关注面包屑以获取更多信息。

运行条件 #

系统可以具有任意数量的运行条件,它们“仅仅”是返回 bool 的系统。如果系统所有运行条件返回的 bool 都是 true,则系统将运行。否则系统将在调度的当前运行中被跳过。

// Let's make our own run condition
fn game_win_condition(query: Query<&Player>, score: Res<Score>) -> bool {
    let player = query.single();
    player.is_alive() && score.0 > 9000
}

app.add_system(win_game.run_if(game_win_condition));

运行条件也有一些“组合器”操作,这要感谢 @JoJoJet@Shatur

它们可以使用 not() 来否定。

app.add_system(continue_game.run_if(not(game_win_condition)))

它们也可以与 and_thenor_else 组合。

app.add_system(move_player.run_if(is_alive.or_else(is_zombie)))

Bevy 0.10 附带了一组很棒的内置通用运行条件。您可以轻松地运行系统,前提是存在要处理的事件、已过期的计时器、已更改的资源、输入状态更改、状态更改等等(感谢 @maniwani@inodentry@jakobhellermann@jabuwu)。

运行条件也可以用作轻量级优化工具。运行条件在主线程上进行评估,每个运行条件在调度的每次更新时,在第一个依赖它的系统集中进行评估。被运行条件禁用的系统不会生成任务,这在许多系统中可能会累积起来。像往常一样:基准测试!

运行条件已取代 Bevy 以前版本中的“运行条件”。我们终于可以摆脱令人讨厌的“循环运行条件”了!ShouldRun::YesAndCheckAgain 对于引擎开发人员或用户来说都不是很直观。当您的类似布尔的枚举具有四个可能值时,这始终是一个不好的迹象。如果您渴望更复杂的控制流:在上面部分中使用“独占系统中的调度”模式。对于其他 99% 的用例,享受更简单的基于 bool 的运行条件!

更简单的状态 #

调度 v3添加了一个新的、更简单的“状态系统”。状态 允许您轻松地配置基于应用程序的当前“状态”运行的不同应用程序逻辑。

您这样定义状态

#[derive(States, PartialEq, Eq, Debug, Clone, Hash, Default)]
enum AppState {
    #[default]
    MainMenu,
    InGame,
}

枚举的每个变体对应于应用程序可以处的不同状态。

您可以这样将状态添加到您的应用程序

app.add_state::<AppState>()

这将设置您的应用程序以使用给定的状态。它添加了 状态 资源,它可以用于查找应用程序当前处于的状态。

fn check_state(state: Res<State<AppState>>) {
    info!("We are in the {} state", state.0);
}

此外,add_state 将为每个可能的值创建一个 OnUpdate 集,然后您可以将您的系统添加到其中。这些集合作为正常应用程序更新的一部分运行,但仅当应用程序处于给定状态时运行。

app
    .add_systems(
        (main_menu, start_game)
            .in_set(OnUpdate(AppState::MainMenu))
    )
    .add_system(fun_gameplay.in_set(OnUpdate(AppState::InGame)));

它还将为每个状态创建 OnEnterOnExit 调度,它们仅在从一种状态转换到另一种状态时运行。

app
    .add_system(load_main_menu.in_schedule(OnEnter(AppState::MainMenu)))
    .add_system(cleanup_main_menu.in_schedule(OnExit(AppState::MainMenu)))

add_state 还添加了 nextState 资源,它可以用来排队状态更改。

fn start_game(
    button_query: Query<&Interaction, With<StartGameButton>>,
    mut next_state: ResMut<NextState<AppState>>,
){
    if button_query.single() == Interaction::Pressed {
        next_state.set(AppState::InGame);
    }
}

这取代了 Bevy 以前的状态系统,该系统很难处理。它具有状态堆栈、精心设计的排队转换和错误处理(大多数人只是取消了包装)。状态堆栈非常复杂,难以学习,非常容易出现令人恼火的错误,而且大多被忽略。

因此,在Bevy 0.10中,状态现在是“无堆栈的”:一次只能排队一种类型的状态。经过大量的 alpha 测试,我们有理由相信这对于迁移来说应该不会太糟糕。如果您依赖于状态堆栈,您有很多选择

  • 在核心状态系统的基础上构建“堆栈”逻辑
  • 将您的状态拆分为多个状态,这些状态捕获应用程序状态的正交元素
  • 使用与 Bevy 的第一方版本相同的模式构建您自己的状态堆栈抽象。新的状态逻辑都没有硬编码!如果您构建了一些东西,让社区其他成员知道,以便您进行协作!

基础集:获得正确的默认行为 #

一位敏锐的读者可能会指出

  1. Bevy 会自动并行运行其系统。
  2. 除非系统之间存在明确的排序关系,否则系统的顺序是非确定性的。
  3. 现在所有系统都存储在一个单一的 Schedule 对象中,它们之间没有障碍。
  4. 系统可以属于任意数量的系统集,每个系统集都可以添加自己的行为。
  5. Bevy 是一个功能强大的引擎,具有许多内部系统。

这不会导致彻底的混乱以及乏味的面条式工作来解决每一个排序歧义?许多用户喜欢阶段,它们有助于理解应用程序的结构!

好吧,我们很高兴您提出这个问题,虚构的怀疑者。为了减少这种混乱(并简化迁移),Bevy 0.10 附带了一组全新的系统集,这些系统集由 DefaultPlugins 提供:CoreSetStartupSetRenderSet。它们名称与旧的 CoreStageStartupStageRenderStage 的相似之处并非巧合。与阶段非常相似,每个集之间都有命令刷新点,并且现有系统已直接迁移。

以阶段为中心的架构的某些部分很有吸引力:清晰的高级结构、在刷新点上的协调(以减少过度的瓶颈)以及良好的默认行为。为了保留这些部分(同时剔除令人沮丧的部分),我们引入了基础集的概念(由 @cart 添加)。基础集只是普通的系统集,除了

  1. 每个系统最多可以属于一个基础集。
  2. 没有指定基础集的系统将被添加到调度的默认基础集(如果调度有一个)。
// You define base sets exactly like normal sets, with the
// addition of the system_set(base) attribute
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
#[system_set(base)]
enum MyBaseSet {
    Early,
    Late,
}

app
    // This ends up in CoreSet::Update by default
    .add_system(no_explicit_base_set)
    // You must use .in_base_set rather than .in_set for explicitness
    // This is a high-impact decision!
    .add_system(post_update.in_base_set(CoreSet::PostUpdate))
    // Look, it works!
    .add_system(custom_base_set.in_base_set(MyBaseSet::Early))
    // Ordering your base sets relative to CoreSet is probably wise
    .configure_set(MyBaseSet::Early.before(CoreSet::Update))
    .configure_set(MyBaseSet::Late.after(CoreSet::Update));

让我给你讲个故事,故事发生在一个没有基础集的世界里

  1. 一名新用户将 make_player_run 系统添加到他们的应用程序中。
  2. 有时这个系统在输入处理之前运行,导致随机丢弃输入。有时它在渲染之后运行,导致奇怪的闪烁。
  3. 经过一番沮丧后,用户发现这是由于“系统执行顺序歧义”。
  4. 用户运行了一个专门的检测工具,深入研究引擎的源代码,找出他们的系统应该在相对于引擎系统集的哪个顺序运行,然后继续他们的快乐旅程,对每个新系统都这样做。
  5. Bevy(或他们的第三方插件之一)更新,再次破坏了我们所有可怜的用户的系统排序。

这说明了一个明显的问题,即大多数游戏系统不需要知道或关心“内部系统”。

我们发现,在实践中,系统主要分为三类:游戏逻辑(所有最终用户系统的大多数)、在游戏逻辑之前需要发生的事情(例如事件清理和输入处理)以及在游戏逻辑之后需要发生的事情(例如渲染和音频)。

通过使用基础集广泛地对调度进行排序,Bevy 应用程序可以具有良好的默认行为和清晰的高级结构,而不会影响高级用户渴望的调度灵活性和明确性。请告诉我们您的使用情况如何!

改进的系统歧义检测 #

当多个系统以冲突的方式与 ECS 资源交互,但它们之间没有排序约束时,我们称之为“歧义”。如果您的 App 存在歧义,这可能会导致错误。我们已显著改善了歧义报告,您可以在新的 ScheduleBuildSettings 中配置它。查看文档以了解更多信息。如果您还没有在您的应用程序上尝试过:您应该看看!

单线程执行 #

现在,您可以通过 SingleThreadedExecutor 轻松地将 Schedule 切换为单线程评估,适用于不需要或不需要并行的用户。

schedule.set_executor_kind(ExecutorKind::SingleThreaded);

级联阴影贴图 #

作者:@danchia,Rob Swain (@superdump)

Bevy 使用“阴影贴图”为灯光/物体投射阴影。以前版本的 Bevy 对定向光源使用了一个简单但有限的阴影贴图实现。对于给定的灯光,您将定义阴影贴图的分辨率和一个手动“视图投影”,它将确定阴影的投射方式。这有一些缺点

  • 阴影贴图的分辨率是固定的。您必须在“覆盖较大的区域,但具有较低的分辨率”和“覆盖较小的区域,但具有较高的分辨率”之间做出选择。
  • 分辨率不会适应相机位置。阴影可能在一个位置看起来很棒,但在另一个位置看起来很糟糕。
  • “阴影投影”必须手动定义。这使得很难且难以接近地配置阴影以匹配给定的场景。

Bevy 0.10 添加了“级联阴影贴图”,它将摄像头的视锥体分解为一系列可配置的“级联”,每个级联都有自己的阴影贴图。这使得“靠近摄像机”的级联中的阴影非常详细,同时允许“远离摄像机”的阴影覆盖更广的区域,但细节更少。因为它使用摄像头的视锥体来定义阴影投影,所以阴影质量在摄像机穿过场景时保持一致。这也意味着用户不再需要手动配置阴影投影。它们会自动计算!

请注意,附近的阴影非常详细,而远处的阴影随着距离的增加而变得不那么详细(这并不重要,因为它们很远)。

虽然阴影级联解决了重要问题,但也引入了新问题。您应该使用多少级联?从摄像机到阴影出现的位置的最小和最大距离是多少?级联之间应该有多少重叠?务必调整这些参数以适应您的场景。

环境贴图照明 #

作者:@JMS55

环境贴图是一种流行且计算成本低廉的方式,可以显著提高场景照明的质量。它使用立方体贴图纹理来提供来自“所有方向”的 360 度照明。这对于反射表面尤其明显,但它适用于所有照明材质。

这是 PBR 材质在没有环境贴图照明的情况下看起来的样子

env map before

这是 PBR 材质在有环境贴图照明的情况下看起来的样子

env map after

对于需要恒定照明(特别是室外场景)的场景,环境贴图是一个很好的解决方案。并且由于环境贴图是任意图像,因此艺术家对场景照明的特征有很大的控制权。

深度和法线预处理 #

作者:@icesentry,Rob Swain (@superdump),@robtfm,@JMS55

此效果使用预处理中的深度来查找地面和力场之间的交点

Bevy 现在能够运行深度和/或法线预处理。这意味着深度和法线纹理将在主处理之前运行的渲染处理中生成,因此可以在主处理期间使用。这使得各种特殊效果成为可能,例如屏幕空间环境光遮蔽、时间抗锯齿等等。这些目前正在开发中,应该 在 Bevy 的下一个版本中可用

Edge detection

在右侧的图像中,绿线是在法线纹理中检测到的边缘,蓝线是在深度纹理中检测到的边缘

Edge detection prepass

预处理生成的深度和法线纹理

使用预处理本质上意味着渲染所有内容两次。预处理本身速度要快得多,因为它执行的工作比主处理少得多。预处理的结果可用于减少主处理中的过度绘制,但如果您的场景本来就不存在过度绘制,那么启用预处理将对性能产生负面影响。有很多方法可以改进这一点,我们将继续朝着这个目标努力。就像与任何与性能相关的事情一样,请务必根据您的用例进行测量,看看它是否有帮助。

当使用需要深度或法线纹理的特殊效果时,预处理仍然非常有用,因此,如果您想使用它,只需将DepthPrepassNormalPrepass 组件添加到您的相机即可。

使用预处理着色器的阴影贴图 #

作者:@geieredgar

以前,用于阴影贴图的着色器是硬编码的,并且对材质没有了解,只有网格。现在,在 Bevy 0.10 中,Material 的深度预处理着色器用于阴影贴图。这意味着用于对 Material 执行阴影贴图的着色器是可自定义的!

作为奖励,在阴影贴图期间可以使用 Material 信息意味着我们可以立即启用 alpha 遮罩阴影,使树叶根据其纹理中的 alpha 值而不是仅基于其几何体来投射阴影。

Alpha mask shadows

平滑的骨骼动画过渡 #

作者:@smessmer

您现在可以在两个(或多个)骨骼动画之间平滑过渡!

角色模型和动画是来自 Mixamo 的免版税资产。

使用 play_with_transition 方法在 AnimationPlayer 组件上,您现在可以指定一个过渡持续时间,在此期间,新动画将与当前正在播放的动画进行线性混合,其权重将在该持续时间内线性下降,直到达到 0.0

#[derive(Component, Default)]
struct ActionTimer(Timer);

#[derive(Component)]
struct Animations {
    run: Handle<AnimationClip>,
    attack: Handle<AnimationClip>,
}

fn run_or_attack(
    mut query: Query<(&mut AnimationPlayer, &mut ActionTimer, &Animations)>,
    keyboard_input: Res<Input<KeyCode>>,
    animation_clips: Res<Assets<AnimationClip>>,
    time: Res<Time>,
) {
    for (mut animation_player, mut timer, animations) in query.iter_mut() {
        // Trigger the attack animation when pressing <space>
        if keyboard_input.just_pressed(KeyCode::Space) {
            let clip = animation_clips.get(&animations.attack).unwrap();
            // Set a timer for when to restart the run animation
            timer.0 = Timer::new(
                Duration::from_secs_f32(clip.duration() - 0.5),
                TimerMode::Once,
            );
            // Will transition over half a second to the attack animation
            animation_player
                .play_with_transition(animations.attack.clone(), Duration::from_secs_f32(0.5));
        }
        if timer.0.tick(time.delta()).just_finished() {
            // Once the attack animation is finished, restart the run animation
            animation_player
                .play_with_transition(animations.run.clone(), Duration::from_secs_f32(0.5))
                .repeat();
        }
    }
}

改进的 Android 支持 #

作者:@mockersf,@slyedoc

Android emulator running Bevy

Bevy 现在可以在更多设备上的 Android 上开箱即用。这是通过等待 Resumed 事件来创建窗口而不是在启动时进行创建来实现的,这与 Android 上的 onResume() 回调相匹配。

为了遵循 Suspended 事件的建议,Bevy 现在将在收到该事件时退出。这是一种临时解决方案,直到 Bevy 能够在恢复时重新创建渲染资源。

请在您的设备上测试并报告您遇到的成功或问题!在某些带有软件按钮的设备上,存在一个关于触摸位置的已知问题,因为 winit 不会暴露 (yet) 内插大小,只暴露内部大小。

由于这使得 Bevy 更接近于完全支持 Android,因此不再需要为 Android 和 iOS 创建单独的示例。它们已在一个 "mobile" 示例 中重新组合,并且说明已更新 (for Androidfor iOS).

以下是在 iOS 上运行的同一个示例!

iOS emulator running Bevy

重新设计的 Bloom #

作者:@StarLederer,@JMS55

Bloom 经历了一些重大变化,现在看起来更好,更容易控制,并且视觉伪像更少。结合新的色调映射选项,Bloom 自上一个版本以来有了很大改进!

  1. 在 Bevy 0.9 中,Bloom 看起来像这样。
  2. 将色调映射器切换到像 AcesFitted 这样的东西已经是很大改进。
  3. 在 Bevy 0.10 中,Bloom 现在看起来像这样。它更可控,也不那么霸道。
  4. 为了使 Bloom 更强,而不是提高 BloomSettings 强度,让我们将每个立方体的 emissive 值加倍。
  5. 最后,如果您想要更极端的 Bloom,类似于旧算法,您可以将 BloomSettings::composite_modeBloomCompositeMode::EnergyConserving 更改为 BloomCompositeMode::Additive
  6. 使用新的 bloom_3d(和 bloom_2d)示例在交互式游乐场中探索新的 Bloom 设置。
1
2
3
4
5
6

距离和大气雾 #

作者:Marco Buono (@coreh)

Bevy 现在可以渲染距离和大气雾效果,通过使物体在距离视野越远的地方显得更暗,从而为您的场景带来更强烈的深度氛围

The new fog example showcases different fog modes and parameters.

雾可以通过新的 FogSettings 组件对每个相机进行控制。在暴露几个旋钮方面已投入了特别的关注,让您可以完全艺术地控制雾的外观,包括通过控制雾颜色的 alpha 通道来控制雾的淡入淡出。

commands.spawn((
    Camera3dBundle::default(),
    FogSettings {
        color: Color::rgba(0.1, 0.2, 0.4, 1.0),
        falloff: FogFalloff::Linear { start: 50.0, end: 100.0 },
    },
));

雾的行为方式,关于距离是由 FogFalloff 枚举控制的。所有来自固定功能 OpenGL 1.x / DirectX 7 时代的“传统”雾衰减模式都受到支持

FogFalloff::Linearstartend 参数之间的 0 到 1 线性增加强度。(此示例分别使用 0.8 和 2.2 的值。)

11023distancefog intensitystartend

FogFalloff::Exponential 根据由 density 参数控制的(反)指数公式增加。

11023density = 2density = 1density = 0.5distancefog intensity

FogFalloff::ExponentialSquared 根据略微修改的(反)指数平方公式增长,也由 density 参数控制。

11023density = 2density = 1density = 0.5distancefog intensity

此外,还提供了一种更复杂的 FogFalloff::Atmospheric 模式,它通过分别考虑光线extinctioninscattering 来提供更物理准确的结果。

DirectionalLight 影响也通过 directional_light_colordirectional_light_exponent 参数支持所有雾模式,模拟在阳光明媚的户外环境中观察到的光线散射效果。

The new atmospheric_fog example showcases a terrain with atmospheric fog and directional light influence.

由于直接“手动”控制非线性雾衰减参数可能很棘手,因此提供了一些基于 气象能见度 的辅助函数,例如 FogFalloff::from_visibility()

FogSettings {
    // objects retain visibility (>= 5% contrast) for up to 15 units
    falloff: FogFalloff::from_visibility(15.0),
    ..default()
}

雾在 PBR 片段着色器上以“前向渲染风格”应用,而不是作为后期处理效果,这使得它能够正确处理半透明网格。

大气雾实现主要基于 Inigo Quilez(Shadertoy 联合创始人兼计算机图形传奇人物)的 这篇很棒的文章感谢您精彩的写作和灵感!

StandardMaterial 混合模式 #

作者:Marco Buono (@coreh)

在 **Bevy 0.10** 中,AlphaMode 枚举得到了扩展,为 StandardMaterial 带来了对加色和乘色混合的支持。这两种混合模式是“经典”(非物理基础)计算机图形学工具箱的主力,通常用于实现各种效果。

演示使用混合模式创建彩色玻璃和火焰效果。 (源代码)

此外,还添加了对具有 预乘 alpha 的半透明纹理的支持,通过专用 alpha 模式实现。

以下是新模式的概览

  • AlphaMode::Add — 以加法方式将片段的颜色与它们后面的颜色混合在一起(例如,像光一样),产生更亮的结果。适用于火焰、全息图、鬼魂、激光和其他能量束等效果。在图形软件中也称为线性叠加
  • AlphaMode::Multiply — 以乘法方式将片段的颜色与它们后面的颜色混合在一起(例如,像颜料一样),产生更暗的结果。适用于近似部分光传输的效果,如彩色玻璃、窗户着色膜和一些彩色液体。
  • AlphaMode::Premultiplied — 行为与 AlphaMode::Blend 非常相似,但假设颜色通道具有预乘 alpha。可以用来避免使用普通 alpha 混合纹理时可能出现的变色“轮廓”伪影,或者巧妙地创建将加色和普通 alpha 混合组合在一个纹理中的材质,因为对于其他恒定的 RGB 值,Premultiplied 对接近 1.0 的 alpha 值表现得更像 Blend,而对接近 0.0 的 alpha 值表现得更像 Add

The new blend_modes example.

注意:使用新混合模式的网格将绘制在现有的 Transparent3d 渲染阶段,因此 AlphaMode::Blend 的相同z 轴排序考虑因素/限制适用。

更多色调映射选择 #

作者:@DGriffin91,@JMS55

色调映射是使用“显示渲染变换”(DRT)将原始高动态范围 (HDR) 信息转换为实际“屏幕颜色”的过程。在 Bevy 的早期版本中,你只有两个色调映射选项:Reinhard 亮度或完全没有。在 **Bevy 0.10** 中,我们添加了许多选择!

无色调映射 #

这通常不推荐,因为 HDR 照明并非旨在用作颜色。

no tonemapping

Reinhard #

一种简单的适应场景中颜色的方法:r = color / (1.0 + color)。色调变化很大,亮度不会自然地去饱和。亮色主色和次色根本不会去饱和。

reinhard

Reinhard 亮度 #

一种流行的方法,类似于普通 Reinhard,但包含了亮度。它适应场景中的光量。这是我们在 Bevy 的早期版本中使用的。它仍然是我们的默认算法,但这可能会在未来发生变化。色调变化。亮度在整个频谱中不会发生很大变化。

reinhard luminance

ACES Fitted #

电影和行业中使用的一种非常流行的算法(例如:ACES 是 Unreal 默认的色调映射算法)。当人们说“电影化”时,通常指的就是这个。

不中性,具有非常特定的美学风格,有意而戏剧性的色调变化。亮绿色和红色变成橙色。亮蓝色变成洋红色。对比度显著提高。亮度在整个频谱中去饱和。

aces

AgX #

非常中性。与其他变换相比,图像有点去饱和。色调变化很小。轻微的 Abney 偏移由 Troy Sobotka 创建

agx

Somewhat Boring Display Transform #

在暗部和中性色中几乎没有色调变化,但在亮部变化很大。亮度在整个频谱中去饱和。有点介于 Reinhard 和 Reinhard 亮度之间。概念上类似于 reinhard-jodie。设计为一种折衷方案,如果你想要例如在弱光下获得不错的肤色,但又无法负担重新制作 VFX 以在没有色调变化的情况下看起来不错。由 Tomasz Stachowiak 创建。

SomewhatBoringDisplayTransform

TonyMcMapface #

非常中性。轻微但有意而戏剧性的色调变化。亮度在整个频谱中去饱和。

摘自作者:Tony 是一种专为实时应用(如游戏)设计的显示变换。它有意地很无聊,不会增加对比度或饱和度,并且会尽可能地保持输入信号,在不需要压缩的情况下。输入信号的亮度等效亮度会受到压缩。非线性类似于 Reinhard。在压缩过程中,颜色色调会保留,但会进行有意而戏剧性的 Bezold–Brücke 偏移。为了避免出现色带现象,会使用选择性去饱和,同时注意避免 Abney 效应由 Tomasz Stachowiak 创建

TonyMcMapface

Blender Filmic #

Blender 中的默认 Filmic 显示变换。有点中性。色调变化。亮度在整个频谱中去饱和。

Blender Filmic

颜色分级控制 #

作者:@DGriffin91

我们添加了一些对颜色分级参数的基本控制,例如曝光度、伽马值、“色调映射前饱和度”和“色调映射后饱和度”。这些可以通过使用新的 ColorGrading 组件为每个相机进行配置。

0.5 曝光度 #

0.5 exposure

2.25 曝光度 #

2.25 exposure

并行流水线渲染 #

作者:@hymm,@james7132

Trace with Pipelined Rendering

在多线程平台上,**Bevy 0.10** 现在将通过并行运行模拟和渲染而显著加快速度。渲染器在 Bevy 0.6 中重新设计,以启用此功能,但真正将它们并行运行的最后一步直到现在才完成。为了解决这个问题,我们进行了一些棘手的操作。渲染世界有一个必须在主线程上运行的系统,但任务池只能在世界的线程上运行。因此,当我们将渲染世界发送到另一个线程时,我们需要容纳仍在主线程上运行的渲染系统。为此,我们添加了在主线程上(除了世界的线程)生成任务的功能。

Histogram of Many Foxes Frame Time

在测试不同的 Bevy 示例时,增益通常在 10% 到 30% 的范围内。如上图直方图所示,“多只狐狸”压力测试的平均帧时间比之前快了 1.8 毫秒。

要使用流水线渲染,只需添加 PipelinedRenderingPlugin。如果你正在使用 DefaultPlugins,那么它会在所有平台上自动添加,除了 wasm。Bevy 目前不支持 wasm 上的多线程,这是此功能正常工作所需的。如果你没有使用 DefaultPlugins,则可以手动添加插件。

将窗口作为实体 #

作者:@aceeri,@Weibye,@cart

在 Bevy 的早期版本中,Window 被表示为一个 ECS 资源(包含在 Windows 资源中)。在 **Bevy 0.10** 中,Window 现在是一个组件(因此窗口被表示为实体)。

这实现了许多目标

  • 它为在 Bevy 的场景系统中表示窗口打开了大门
  • 它将 Windows 暴露给 Bevy 的强大 ECS 查询
  • 它提供了对每个窗口的粒度变更检测
  • 改进了创建、使用和关闭窗口的可读性/可发现性
  • 更改窗口的属性对于初始化和修改都是一样的。不再需要 WindowDescriptor 的麻烦!
  • 它允许 Bevy 开发人员和用户轻松地将新的组件数据附加到窗口
fn create_window(mut commands: Commands) {
    commands.spawn(Window {
        title: "My window :D".to_string(),
        ..default()
    });
}

fn modify_windows(mut windows: Query<&mut Window>) {
    for window in &mut windows {
        window.title = "My changed window! :D".to_string();
    }
}

fn close_windows(mut commands: Commands, windows: Query<Entity, With<Window>>) {
    for entity in &windows {
        commands.entity(entity).despawn();
    }
}

渲染器优化 #

作者:@danchia,Rob Swain (@superdump),james7132,@kurtkuehnert,@robfm

Bevy 的渲染器已经成熟到可以进行优化了。因此,我们对其进行了优化!

在 Bevy 中渲染任何内容时,最大的瓶颈是最终的渲染阶段,我们收集渲染世界中的所有数据以向 GPU 发送绘制调用。这里的核心循环非常热,任何额外的开销都会很明显。在 **Bevy 0.10** 中,我们对这个问题投入了大量精力,并从各个角度进行了攻击。总的来说,以下优化应该使渲染阶段比 0.9 版本快2-3 倍

  • #7639 中,@danchia 发现,即使禁用了日志记录,对热循环也会产生很大影响,在该阶段使速度提高了 20-50%。
  • #6944 中,@james7132 缩小了该阶段涉及的核心数据结构,减少了内存获取,使速度提高了 9%。
  • #6885 中,@james7132 重新设计了我们的 PhaseItemRenderCommand 基础设施,以在从 World 获取组件数据时组合常见的操作,使速度提高了 7%。
  • #7053 中,@james7132 更改了 TrackedRenderPass 的分配模式,以最大程度地减少这些循环中的分支,使速度提高了 6%。
  • #7084 中,@james7132 改变了我们从 World 获取资源的方式,以最大程度地减少在该阶段使用原子操作,使速度提高了 2%。
  • #6988 中,@kurtkuehnert 更改了我们的内部资源 ID,使用原子递增计数器代替 UUID,减少了该阶段中一些分支的比较成本。

另一个正在进行的开发是使渲染阶段能够跨多个线程正确地并行化命令编码。继 #7248 由 @james7132 完成之后,我们现在支持将外部创建的 CommandBuffer 导入渲染图,这应该允许用户并行编码 GPU 命令并将它们导入渲染图。目前,这受到 wgpu 的阻碍,wgpu 在编码渲染通道时会锁定 GPU 设备,但我们应该能够在问题解决后尽快支持并行命令编码。

同样,我们也采取了措施,以在渲染管道的其他阶段启用更高的并行性。PipelineCache 是一种资源,几乎每个 Queue 阶段系统都需要以可变的方式访问,但也只需要很少地写入。在 #7205 中,@danchia 将其更改为使用内部可变性,以允许这些系统并行化。这还不能完全允许该阶段的每个系统都并行化,因为仍然存在一些常见的阻碍因素,但它应该允许无冲突的渲染阶段同时排队命令。

优化不仅仅是 CPU 时间!我们还改进了内存使用、编译时间和 GPU 性能!

  • 由于 @james7132 的贡献,我们还将 ComputedVisibility 的内存使用量减少了 50%。这是通过用一组位标志替换内部存储来完成的,而不是使用多个布尔值。
  • @robfm 还使用类型擦除作为一种解决方法,以解决 rustc 性能回归,以确保渲染相关的 crate 具有更好的编译时间,其中一些 crate 的编译速度提高了 高达 60%!完整的细节可以在 #5950 中查看。
  • #7069 中,Rob Swain (@superdump) 减少了 GPU 上使用的活动寄存器的数量,以防止寄存器溢出,从而显着提高了 GPU 端的性能。

最后,我们对特定使用场景进行了一些改进

  • #6833 中,@james7132 通过省略不必要的缓冲区复制,将网格蒙皮的骨骼提取速度提高了 40-50%。
  • #7311 中,@james7132 通过将一个常用计算从热循环中提取出来,将 UI 提取速度提高了 33%。

并行化变换传播和动画运动学 #

作者:@james7132

变换传播是任何游戏引擎的核心系统之一。如果你移动一个父实体,你希望它的子级在世界空间中移动。Bevy 的变换传播系统恰好是多个系统的最大瓶颈之一:渲染、UI、物理、动画等系统在它完成之前无法运行。变换传播必须足够快才能避免阻塞所有这些系统。在 Bevy 0.9 及之前的版本中,变换传播始终是单线程的,并且始终需要完整的层次结构遍历。随着世界变得越来越大,这种关键瓶颈所花费的时间也越来越长。在 Bevy 0.10 中,变换传播利用了结构良好的层次结构的结构,可以在多个线程上完全运行。完整的性能优势完全取决于层次结构的结构以及可用的 CPU 内核数量。在我们的测试中,这使得我们 many_foxes 基准测试中的变换传播在我们测试的硬件上 快了 4 倍

如果变换传播可以并行化,那么动画的正向运动学也可以并行化。我们利用了结构良好的层次结构的相同保证结构,以完全并行化播放骨骼动画。我们还启用了一个基本的实体路径缓存查找,以减少系统进行的额外查找。总的来说,我们能够使相同 many_foxes 基准测试中的动画播放系统 快了 10 倍

结合本版本中看到的其他所有优化,我们对 many_foxes 基准测试的测试已经从每帧约 10 毫秒(约 100 FPS)提高到每帧约 2.3 毫秒(约 434 FPS),速度提高了近 5 倍!

ECS 优化 #

作者:@james7132、@JoJoJet

ECS 是整个引擎的基础,因此消除 ECS 中的开销会导致引擎范围内的加速。在 Bevy 0.10 中,我们发现了很多领域可以大幅减少开销,并提高整个引擎的 CPU 利用率。

#6547 中,我们启用了 自动向量化,当使用 Query::for_each 及其并行变体时。根据正在编译引擎的目标架构,这会导致查询迭代时间快 50-87.5%。在 0.11 中,我们可能会将这种优化扩展到所有基于 Iterator::fold 的迭代器组合器,例如 Iterator::count。有关更多详细信息,请参阅 此 PR

#6681 中,通过紧密打包实体位置元数据并避免额外的内存查找,我们显着减少了通过 Query::get 进行随机查询查找时的开销,在 Query::getWorld::get 中花费的开销减少了高达 43%。

#6800#6902 中,我们发现 rustc 可以优化掉跨函数边界的编译时常量分支,将分支从运行时移动到编译时,从而在使用 EntityRef::getEntityMut::insertEntityMut::remove 及其变体时,将开销减少了高达 50%。

#6391 中,我们重新设计了 CommandQueue 的内部结构,使其更符合 CPU 缓存友好,这表明在编码和应用命令时速度提高了高达 37%。

SystemParam 改进 #

作者:@JoJoJet

Bevy 的 ECS 的核心是 SystemParam:这些类型(例如 QueryRes)决定了系统可以做什么和不能做什么。以前,手动创建一个需要实现四个不可分割的特征的家族。在 Bevy 0.10 中,我们 使用了泛型关联类型,将它们 减少到只有两个特征SystemParamReadOnlySystemParam

此外,#[derive(SystemParam)] 宏还接受了许多杂项可用性改进

  • 更灵活:你不再被迫声明你不使用的生命周期。元组结构体现在允许使用,并且 const 泛型不会破坏任何东西。
  • 封装:一个长期存在的错误已得到修复,该错误泄露了私有字段的类型。现在,SystemParam 可以正确地封装私有世界数据。
  • 无限:已取消 16 个字段限制,因此你可以使你的参数变得尽可能复杂。这对于生成代码最有用。

延迟世界修改 #

作者:@JoJoJet

你可能知道,当你发送一个 Command 时,它不会立即修改世界。该命令存储在系统中,并在稍后的调度中应用。以这种方式延迟修改有一些好处

  • 最小化世界访问:与可变查询(和资源)不同,延迟修改不受数据访问冲突的影响,这使得使用此模式的系统能够更好地并行化。
  • 顺序独立性:当执行幂等操作(如设置全局标志)时,延迟修改允许你不必担心系统的执行顺序。
  • 结构修改:延迟修改能够以 QueryResMut 无法做到的方式改变世界的结构,例如添加组件或生成和销毁实体。

Bevy 0.10 通过 Deferred 系统参数为此模式添加了头等支持,该参数接受 SystemBuffer 特征实现。这使你能够创建具有自定义延迟修改行为的系统,同时跳过与 Commands 相关的开销!

/// Sends events with a delay, but can run in parallel with other event writers.
pub struct EventBuffer<E>(Vec<E>);

// The `SystemBuffer` trait controls how deferred mutations get applied to the world.
impl<E> SystemBuffer for EventBuffer<E> { ... }

fn my_system(mut events: Deferred<EventBuffer<MyEvent>>) {
    // Queue up an event to get sent when commands are applied.
    events.0.push(MyEvent);
}

请注意,此功能应谨慎使用 - 尽管有潜在的性能优势,但使用不当实际上会导致性能 下降。在执行任何优化时,请确保你确实提高了速度!

Ref<T> 查询 #

作者:@Guvante、@JoJoJet

自从 Bevy 0.1 以来,Mut<T> 一直用于启用更改检测(以及相关的类型,如 ResMut<T>)。它是一个简单的包装类型,它提供了对组件及其更改标记元数据的可变访问,在值被修改时会自动标记更改。

Bevy 0.10 中,更改检测家族已经扩展到 Ref<T>,它是 Mut<T> 的不可变变体。与它的可变同胞一样,它允许你对当前系统之外做出的更改做出反应。

use bevy::prelude::*;

fn inspect_changes_system<T: Component + Debug>(q: Query<Ref<T>>) {
    // Iterate over each component of type `T` and log its changed status.
    for val in &q {
        if val.is_changed() {
            println!("Value `{val:?}` was last changed at tick {}.", val.last_changed());
        } else {
            println!("Value `{val:?}` is unchanged.");
        }
    }
}

我们还弃用了 ChangeTrackers<T>,这是检查组件更改标记的旧方法。此类型将在 Bevy 的下一个版本中删除。

三次曲线 #

作者:@aevyrie

此视频展示了四种三次曲线使用贝塞尔缓动平滑地动画。曲线本身是白色的,绿色是速度,红色是加速度,蓝色是确定曲线形状的控制点。

为了准备 UI 动画和手动调整的动画曲线,三次曲线已添加到 bevy_math 中。该实现提供了开箱即用的多种曲线,在各种应用程序中很有用

  • Bezier:用户绘制的样条曲线,以及用于 UI 的三次贝塞尔动画缓动 - 为三次动画缓动提供了辅助方法,如上图所示。
  • Hermite:在你知道位置和速度的两个时间点之间进行平滑插值,例如网络预测。
  • Cardinal:在任意数量的控制点之间进行轻松插值,自动计算切线;Catmull-Rom 是一种 Cardinal 样条曲线。
  • B-Spline:加速度连续运动,特别适用于摄像机路径,其中速度(加速度)的平滑变化对于防止剧烈的抖动运动很重要。

CubicGenerator 特征是公开的,允许你定义自己的自定义样条曲线,这些样条曲线会生成 CubicCurve

性能 #

可以评估 CubicCurve 的位置、速度和加速度在任意点。这些评估都具有相同的性能成本,无论使用的三次曲线的类型如何。在现代 CPU 上,这些评估需要 1-2 纳秒,动画缓动(这是一个迭代过程)需要 15-20 纳秒。

将 AccessKit 集成到 bevy_ui#

作者:@ndarilek

游戏是为所有人准备的:游戏的构建方式也应反映这一点。无障碍游戏很少见,适当的支持通常是事后才想到的,无论是从引擎还是游戏的角度。通过在构建 UI 解决方案时将可访问性放在首位,我们希望能够解决这个问题。

Bevy 在优秀 AccessKit crate 的帮助下,加入了 egui,迈出了迈向跨平台默认可访问性的第一步。据我们所知,这使得 Bevy 成为第一个具有第一方可访问性支持的通用游戏引擎。

我们已经将 Bevy 的 UI 层次结构和文本元素公开给了屏幕阅读器和其他辅助设备,由新的默认开启 bevy_a11y crate 管理。这最终由新的 AccessibilityNode 组件提供支持,该组件与现有层次结构结合在一起,将此信息直接公开给 AccessKit 和 Focus 资源,该资源存储具有键盘焦点的实体。

这里还有很多工作要做:将焦点系统与 基于游戏手柄的 UI 控件 解决方案集成,清理数据模型以 确保“默认可访问”成为现实,并添加对 AccessKit 中剩余功能的支持。

特别感谢 @mwcampbell(AccessKit 的主要作者),他审查了我们的集成,并与我们一起努力减少了上游的依赖项,大大改进了编译时间和最终可执行文件的大小。这 在 Linux 上仍然是一个严峻的挑战,因此 accesskit_unix 特征标志 目前默认情况下处于禁用状态

空间音频 #

作者:@mockersf、@DGriffin91、@harudagondi、@alice-i-cecile

Bevy 用於音訊的程式庫 rodio 包含對空間音訊的支持。Bevy 0.10 公開了基本的空間音訊。仍然存在一些注意事項,例如沒有 HRTF,也沒有對 EmitterListener 組件的一流支持。

有趣的是,在開發這個特定功能期間,@harudagondi 發現了一個 錯誤,其中音訊通道在調試或發布模式下運行應用程式時會反轉。這證明是一個 rodio 問題,而且也會影響以前版本的 Bevy。感謝 @dis-da-moe,這個錯誤已在 上游修復。請參閱鏈接的 PR,了解有關音訊程式設計怪癖和性能問題的有趣詳細信息。

您現在可以在遊戲中使用空間音訊!克隆 bevy 倉庫,并在命令行中調用 cargo run --example spatial_audio_3d --release,以展示 Bevy 中的 3D 空間音訊。

自訂音訊來源 #

作者:@dis-da-moe

Bevy 通過 Decodable 特性支持自訂音訊來源,但注冊到 bevy 應用的方式非常冗長,而且文檔很少。在 Bevy 0.10 中,為 App 添加了一個新的擴展特性,並且 Decodable 的文檔得到了很大改善。

因此,不是這樣做

struct MyCustomAudioSource { /* ... */ }

app.add_asset::<MyCustomAudioSource>()
    .init_resource::<Audio<MyCustomAudioSource>>()
    .init_resource::<AudioOutput<MyCustomAudioSource>>()
    .add_system(play_queued_audio_system::<MyCustomAudioSource>.in_base_set(CoreSet::PostUpdate))

您只需要這樣做

app.add_audio_source::<MyCustomAudioSource>()

乾淨多了!

ShaderDef 值 #

作者:@mockersf

Bevy 的著色器處理器現在支持帶有值的 ShaderDefs,使用新的 ShaderDefVal。這允許開發人員將常數值傳遞到他們的著色器中

let shader_defs = vec![
    ShaderDefVal::Int("MAX_DIRECTIONAL_LIGHTS".to_string(), 10),
];

這些可以在 #if 語句中使用,以根據值有選擇地啟用著色器代碼

#if MAX_DIRECTIONAL_LIGHTS >= 10
let color = vec4<f32>(1.0, 0.0, 0.0, 1.0);
#else
let color = vec4<f32>(0.0, 1.0, 0.0, 1.0);
#endif

ShaderDef 值可以內聯到著色器中

for (var i: u32 = 0u; i < #{MAX_DIRECTIONAL_LIGHTS}; i = i + 1u) {
}

它們也可以在著色器中內聯定義

#define MAX_DIRECTIONAL_LIGHTS 10

在著色器中定義的 ShaderDefs 會覆蓋從 Bevy 傳遞的值。

#else ifdef 著色器中的鏈 #

作者:@torsteingrindvik

Bevy 的著色器處理器現在也支持這樣的 #else ifdef

#ifdef FOO
// foo code
#else ifdef BAR
// bar code
#else ifdef BAZ
// baz code
#else
// fallback code
#endif

新的著色器導入:全局和視圖 #

作者:@torsteingrindvik

GlobalView 結構現在可以使用 #import bevy_render::globals#import bevy_render::view 在著色器中導入。Bevy 的內部著色器現在使用這些導入(節省了大量冗余)。以前,您要么需要在每個著色器中重新定義,要么導入更大的 bevy_pbr::mesh_view_types(這並不總是需要的)。

以前需要這樣做

struct View {
    view_proj: mat4x4<f32>,
    inverse_view_proj: mat4x4<f32>,
    view: mat4x4<f32>,
    inverse_view: mat4x4<f32>,
    projection: mat4x4<f32>,
    inverse_projection: mat4x4<f32>,
    world_position: vec3<f32>,
    // viewport(x_origin, y_origin, width, height)
    viewport: vec4<f32>,
};

現在您只需要這樣做!

#import bevy_render::view

自適應批處理,用于并行查詢迭代 #

作者:@james7132

Query::par_for_each 一直是大家在查詢過大而無法單線程運行時求助的工具。在您的屏幕上運行 100,000 個實體嗎?沒問題,Query::par_for_each 將其分成更小的批次,并将工作負載分布在多個線程上。但是,在 Bevy 0.9 及更早版本中,Query::par_for_each 要求調用者提供批次大小,以幫助調整這些批次以獲得最佳性能。這個相當不透明的旋鈕經常導致使用者隨機選擇一個值并使用它,或者根據他們的開發機器微調該值。不幸的是,最有效的值取決于運行時環境(即玩家計算機有多少個邏輯核心)和 ECS 世界的狀態(即有多少實體匹配?)。最終,大多數 API 使用者只選擇一個固定數字,並忍受結果,無論好壞。

// 0.9
const QUERY_BATCH_SIZE: usize = 32;

query.par_for_each(QUERY_BATCH_SIZE, |mut component| {
   // ...
});

在 0.10 中,您不再需要提供批次大小!如果您使用 Query::par_iter,Bevy 將自動評估世界的狀態和任務池,并選擇一個批次大小, 使用啟發式方法 來確保足夠的并行性,而不會產生太多開銷。這使得并行查詢與普通的單線程查詢一樣易於使用!雖然對於大多數典型用例來說很棒,但這些啟發式方法可能并不適合所有工作負載,因此我們為那些需要更精細控制工作負載分布的人提供了一個逃生艙。將來,我們可能會進一步調整支持啟發式方法,以嘗試使默認值更接近這些工作負載中的最佳值。

// 0.10
query.par_iter().for_each(|component| {
   // ...
});

// Fairly easy to convert from a single-threaded for_each. Just change iter to par_iter!
query.iter().for_each(|component| {
   // ...
});

您也可以使用 BatchingStrategy 更精細地控制批處理

query
    .par_iter_mut()
    // run with batches of 100
    .batching_strategy(BatchingStrategy::fixed(100))
    .for_each(|mut component| { /* ... */ });

請參閱 BatchingStrategy 文檔以了解更多信息。

UnsafeWorldCellUnsafeEntityCell #

作者:@jakobhellermann、@BoxyUwU 和 @JoJoJet

UnsafeWorldCellUnsafeEntityCell 允許通過不安全的代碼共享可變訪問世界的一部分。它的作用與 UnsafeCell 類似,允許人們構建內部可變性抽象,例如 Cell Mutex Channel 等。在 bevy 中,UnsafeWorldCell 將用於支持调度器和系統參數實現,因為這些是 World 的內部可變性抽象,它目前也用於實現 WorldCell。我們計劃使用 UnsafeEntityCell 實現僅訪問實體上組件而不是整個世界的 EntityRef/EntityMut 版本。

這些抽象是在 #6404#7381#7568 中引入的。

圓柱體形狀 #

作者:@JayPavlina、@rparrett、@davidhof

圓柱體形狀原語已加入我們的內置形狀動物園!

primitive shapes

可細分的平面形狀 #

作者:@woodroww

Bevy 的 Plane 形狀現在可以細分任意次。

plane

相機輸出模式 #

作者:@cart、@robtfm

Bevy 0.9添加的 相機驅動 后處理功能,為場景中多個相機提供了直觀的后期處理控制,但 存在一些邊緣情況,這些情況并不完全適合硬編碼的相機輸出模型。還有一些與雙緩沖目標紋理的真相源在相機之間不一致以及 MSAA 的采樣紋理在某些情況下沒有包含應有內容相關的錯誤和限制。

Bevy 0.10CameraOutputMode 字段中添加了 Camera,這使 Bevy 應用程式開發人員能夠手動配置 Camera 的渲染結果應如何(以及是否)寫入最終輸出紋理

// Configure the camera to write to the final output texture
camera.output_mode = CameraOutputMode::Write {
    // Do not blend with the current state of the output texture
    blend_state: None,
    // Clear the output texture
    color_attachment_load_op: LoadOp::Clear(Default::default()),
};

// Configure the camera to skip writing to the final output texture
// This can save a pass when there are multiple cameras, and can be useful for
// some post-processing situations
camera.output_mode = CameraOutputMode::Skip;

大多數單相機和多相機設置根本不需要接觸此設置。但是,如果您需要它,它就在那里等著您!

MSAA 需要額外的中間“多采樣”紋理,該紋理被解析為“實際”非采樣紋理。在一些渲染到相同紋理的邊緣情況多相機設置中,這可能會根據是否啟用或禁用 MSAA 產生奇怪/不一致的結果。我們添加了一個新的 Camera::msaa_writeback bool 字段,該字段(啟用時)將將非采樣紋理的當前狀態寫入中間 MSAA 紋理(如果先前的相機已在給定幀上渲染到目標)。這確保了狀態一致,而不管 MSAA 配置如何。這默認為 true,因此您只需要在有多個相機設置并且您想要 MSAA 回寫的情況下考慮它。

可配置的可见性组件 #

作者:@ickk

Visibility 組件控制是否應該渲染 EntityBevy 0.10 重做了類型定義:我們不再使用單個 is_visible: bool 字段,而是使用一個枚舉,其中包含一個額外的模式

pub enum Visibility {
  Hidden,    // unconditionally hidden
  Visible,   // unconditionally visible
  Inherited, // inherit visibility from parent
}

更容易理解!在以前的 Bevy 版本中,“繼承的可見性”和“隱藏”本質上是唯一的兩個選項。現在,實體可以选择可见,即使它们的父级是隐藏的!

AsBindGroup 存儲緩沖區 #

作者:@IceSentry、@AndrewB330

AsBindGroup 是一個有用的 Bevy 特性,它 使將數據傳遞到著色器變得非常容易

Bevy 0.10 通過支持“存儲緩沖區綁定”擴展了這一點,這在傳遞大量/無界塊數據時非常有用

#[derive(AsBindGroup)]
struct CoolMaterial {
    #[uniform(0)]
    color: Color,
    #[texture(1)]
    #[sampler(2)]
    color_texture: Handle<Image>,
    #[storage(3)]
    values: Vec<f32>,
    #[storage(4, read_only, buffer)]
    buffer: Buffer,
}

ExtractComponent 派生 #

作者:@torsteingrindvik

為了將組件數據從“主應用程式”傳遞到“渲染應用程式”以進行 流水線渲染,我們執行“提取步驟”。ExtractComponent 特性用於復制數據。在以前的 Bevy 版本中,您必須手動實現它,但現在您可以派生它!

#[derive(Component, Clone, ExtractComponent)]
pub struct Car {
    pub wheels: usize,
}

這擴展到這個

impl ExtractComponent for Car
{
    type Query = &'static Self;
    type Filter = ();
    type Out = Self;
    fn extract_component(item: QueryItem<'_, Self::Query>) -> Option<Self::Out> {
        Some(item.clone())
    }
}

它還支持過濾器!

#[derive(Component, Clone, ExtractComponent)]
#[extract_component_filter(With<Fuel>)]
pub struct Car {
    pub wheels: usize,
}

將 wgpu 升級到 0.15 #

作者:@Elabajaba

Bevy 0.10 現在使用最新最好的 wgpu(我們選擇的低級圖形層)。除了 許多不錯的 API 改進和錯誤修復 之外,wgpu 現在對 DX12 使用 DXC 著色器編譯器,它更快、更少錯誤,并且允許使用新功能。

默认启用 OpenGL 后端 #

作者:@wangling12

Bevy 一直支持 wgpu 的 OpenGL 后端,但它需要選擇加入。這導致 Bevy 無法在某些不支持 Vulkan 等現代 API 的機器上啟動。在 Bevy 0.10 中,OpenGL 后端默認情況下是啟用的,這意味著機器將在沒有其他 API 可用時自動回退到 OpenGL。

公开非统一索引支持(无绑定) #

作者:@cryscan

Bevy 0.10 連接了對紋理和存儲緩沖區的非統一索引的初始支持。這是邁向現代 "無綁定/ GPU 驅動渲染" 的重要一步,這可以在支持它的平台上釋放顯著的性能。請注意,這只是使功能可供渲染插件開發人員使用。Bevy 的核心渲染功能尚未(還)使用無綁定方法。

我們添加了一個 新的示例,說明如何使用此功能

texture binding array

游戏手柄 API 改进 #

作者:@DevinLeamy

GamepadEventRaw 類型已刪除,取而代之的是單獨的 GamepadConnectionEventGamepadAxisChangedEventGamepadButtonChangedEvent,并且內部已重新設計以適應這種變化。

這允許在不篩選一般 GamepadEvent 類型的 情況下,更簡單、更細粒度的事件訪問。不錯!

fn system(mut events: EventReader<GamepadConnectionEvent>)
    for event in events.iter() {
    }
}

输入法编辑器 (IME) 支持 #

作者:@mockersf

Window 現在可以使用 ime_enabledime_position 配置 IME 支持,這啟用了“死鍵”的使用,這為法語、拼音等提供了支持

反射路径:枚举和元组 #

作者:@MrGVSV

Bevy 的“反射路徑”允許使用簡單(且動態)的字符串語法導航 Rust 值。Bevy 0.10 通過在反射路徑中添加對元組和枚舉的支持來擴展此系統

#[derive(Reflect)]
struct MyStruct {
  data: Data,
  some_tuple: (u32, u32),
}

#[derive(Reflect)]
enum Data {
  Foo(u32, u32),
  Bar(bool)
}

let x = MyStruct {
  data: Data::Foo(123),
  some_tuple: (10, 20),
};

assert_eq!(*x.path::<u32>("data.1").unwrap(), 123);
assert_eq!(*x.path::<u32>("some_tuple.0").unwrap(), 10);

预解析的反射路径 #

作者: @MrGVSV, @james7132

反射路径可以实现很多有趣且动态的编辑器场景,但它们也存在一个缺点:每次调用 path() 都需要解析字符串。为了解决这个问题,我们添加了 ParsedPath,它允许预解析路径并在每次访问时重复使用解析结果。

let parsed_path = ParsedPath::parse("foo.bar[0]").unwrap();
let element = parsed_path.element::<usize>(&some_value);

这更适合重复访问,例如在每一帧都进行相同的查找!

ReflectFromReflect #

作者:@MrGVSV

在使用 Bevy 的 Rust 反射系统时,我们有时会遇到这样的情况:我们有一个表示特定类型 MyType 的“动态反射值”(尽管在幕后,它并不真正是那个类型)。当我们调用 Reflect::clone_value、使用反射反序列化器或自己创建动态值时,就会发生这种情况。不幸的是,我们不能简单地调用 MyType::from_reflect,因为我们没有在运行时了解具体 MyType 的知识。

ReflectFromReflectTypeRegistry 中一个新的“类型数据”结构,它可以在没有任何对给定类型的具体引用情况下实现 FromReflect 特性操作。非常酷!

#[derive(Reflect, FromReflect)]
#[reflect(FromReflect)] // <- Register `ReflectFromReflect`
struct MyStruct(String);

let type_id = TypeId::of::<MyStruct>();

// Register our type
let mut registry = TypeRegistry::default();
registry.register::<MyStruct>();

// Create a concrete instance
let my_struct = MyStruct("Hello world".to_string());

// `Reflect::clone_value` will generate a `DynamicTupleStruct` for tuple struct types
// Note that this is _not_ a MyStruct instance
let dynamic_value: Box<dyn Reflect> = my_struct.clone_value();

// Get the `ReflectFromReflect` type data from the registry
let rfr: &ReflectFromReflect = registry
  .get_type_data::<ReflectFromReflect>(type_id)
  .unwrap();

// Call `FromReflect::from_reflect` on our Dynamic value
let concrete_value: Box<dyn Reflect> = rfr.from_reflect(&dynamic_value);
assert!(concrete_value.is::<MyStruct>());

其他反射改进 #

作者: @james7132, @soqb, @cBournhonesque, @SkiFire13
  • Reflect 现在已为 std::collections::VecDeque 实现。
  • 反射的 List 类型现在具有 insertremove 操作。
  • 反射的 Map 类型现在具有 remove 操作。
  • 反射的泛型类型现在会自动实现 Reflect,如果泛型也实现了 Reflect。无需添加手动 T: Reflect 约束!
  • 组件反射现在使用 EntityRef / EntityMut 而不是同时使用 WorldEntity,这允许它在更多场景中使用。
  • 反射反序列化器现在可以避免在某些情况下不必要地克隆字符串!

Taffy 升级到 0.3 #

作者: @ickshonpe, @rparret

Taffy 是我们用来计算 bevy_ui 布局的库。Taffy 0.2 显著提高了嵌套 UI 的性能(我们的 many_buttons 示例现在快了 8%,更深层次的嵌套 UI 应该会看到更大的提升!)。它还支持 gap 属性,这使得创建具有均匀间距项目的 UI 变得更加容易。Taffy 0.3 添加了一些不错的 API 调整(还添加了一个网格布局功能,我们目前已将其禁用,因为它还需要一些集成工作)。

相对光标位置 #

作者: @Pietrek14

我们添加了一个新的 RelativeCursorPosition UI 组件,它在添加到 UI 实体时会跟踪相对于节点的光标位置。Some((0, 0)) 表示节点的左上角,Some((1,1)) 表示节点的右下角,None 表示光标“在节点之外”。

commands.spawn((
    NodeBundle::default(),
    RelativeCursorPosition::default(),
));

常量 Bevy UI 默认值 #

作者: @james-j-obrien

Bevy 广泛使用 Default 特性来简化类型的构造。Bevy UI 类型通常实现 Default。但是,它有一个缺点(这是 Rust 的根本问题):Default 不能在 const 上下文中使用 (尚未!)。为了使 UI 布局配置可以定义为常量,我们在大多数 Bevy UI 类型中添加了 DEFAULT 关联常量。例如,您可以使用 Style::DEFAULT 来定义一个常量样式。

const COOL_STYLE: Style = Style {
    size: Size::width(Val::Px(200.0)),
    border: UiRect::all(Val::Px(2.0)),
    ..Style::DEFAULT
};

遍历世界的实体 #

作者:@james7132

Bevy 0.9 中,World::iter_entities 允许用户获取 World 中所有实体的迭代器,以 Entity 形式。在 Bevy 0.10 中,这已更改为对 EntityRef 的迭代器,它提供对所有实体组件的完整只读访问,而不仅仅是获取其 ID。它的新实现也应该比手动获取 EntityRef 快得多(尽管请注意,如果您知道要查找的确切组件,则 Query 仍然会更快)。这使用户可以任意从世界中读取任何实体数据,并且可能会在脚本语言集成和反射密集型工作流程中得到应用。

// Bevy 0.9
for entity in world.iter_entities() {
   if let Some(entity_ref) = world.get_entity(entity) {
      if let Some(component) = entity_ref.get::<MyComponent>() {
         ...
      }
   }
}

// Bevy 0.10
for entity_ref in world.iter_entities() {
   if let Some(component) = entity_ref.get::<MyComponent>() {
      ...
   }
}

将来,我们可能会有一个 World::iter_entities_mut,它公开此功能,但提供对 World 中所有实体的任意可变访问。我们目前避免实现这一点,因为返回 EntityMut 迭代器可能会存在安全问题。有关更多详细信息,请参阅此 GitHub 问题

LCH 颜色空间 #

作者: @ldubos

Bevy 的 Color 类型现在支持 LCH 颜色空间(亮度、色度、色调)。LCH 有很多论据支持它,包括它提供了比 sRGB 多约 50% 的颜色。请查看 这篇文章,了解更多信息。

Color::Lcha {
    lightness: 1.0,
    chroma: 0.5,
    hue: 200.0,
    alpha: 1.0,
}

优化 Color::hex 性能 #

作者: @wyhaya

Color::hex 现在是一个 const 函数,这使 hex 的运行时间从大约 14ns 降至大约 4ns!

拆分 CorePlugin #

作者: @targrub

CorePlugin 从历史上来说一直是一个“厨房水槽插件”。“核心”事物如果找不到其他地方,最终都会放在那里。这不是一个很好的组织策略,所以我们将它拆分为单独的部分:TaskPoolPluginTypeRegistrationPluginFrameCountPlugin

EntityCommand #

作者:@JoJoJet

Commands 是“延迟 ECS”操作。它们使开发人员能够定义自定义 ECS 操作,这些操作在并行系统运行完毕后应用。许多 Commands 在单个实体上运行,但这种模式有点繁琐。

struct MyCustomCommand(Entity);

impl Command for MyCustomCommand {
    fn write(self, world: &mut World) {
        // do something with the entity at self.0
    }
}

let id = commands.spawn(SpriteBundle::default()).id();
commands.add(MyCustomCommand(id));

为了解决这个问题,在 Bevy 0.10 中,我们添加了 EntityCommand 特性。这允许命令以符合人体工程学的方式应用于生成的实体。

struct MyCustomCommand;

impl EntityCommand for MyCustomCommand {
    fn write(self, id: Entity, world: &mut World) {
        // do something with the given entity id
    }
}

commands.spawn(SpriteBundle::default()).add(MyCustomCommand);

像素完美示例 #

作者: @Ian-Yy

我们现在有一个新的 “像素完美”示例,说明了如何设置像素完美精灵。它使用了 Bevy 的新可爱徽标精灵!

pixel perfect

UI 文本布局示例 #

作者: @ickshonpe

我们添加了一个很好的 “文本布局”示例,它说明了各种 Bevy UI 文本布局设置。

text layout

CI 改进 #

作者:@mockersf

我们在 Bevy 领域非常重视 CI,并且一直在寻找让我们的生活更美好的新方法。在本周期中,我们进行了一些不错的改进。

  • 我们现在为 bevy crate 设置了一个 MSRV(最低支持 Rust 版本),并且有一个 CI 任务检查 MSRV。
  • CI 会向新贡献者发送友好的欢迎信息!
  • CI 现在会在 PR 被标记为重大更改并且没有迁移指南时要求提供迁移指南。

第一个主题专家版本 #

这是我们使用新的 主题专家 (SME) 系统 发布的第一个版本。我们合并了大量更改,并且这尽管我们的项目负责人 @cart 由于圣诞节和滑雪度假而缺席了大约一个月。我们保持了高标准,并且创造了令人惊叹的东西。可以肯定地说,未来一片光明(并且可持续)!敬请关注更多领域的更多 SME 职位。

下一步是什么? #

  • 资产系统演变:我们在 Bevy 资产系统的下一个迭代 上取得了进展,这将添加预处理资产的能力,并提高资产系统的灵活性和可用性。
  • 启动 Bevy 编辑器工作:我们已准备好开始将重点转移到构建 Bevy 编辑器!我们已经开始 收集需求,并且希望在 Bevy 0.11 周期中开始初始设计阶段。
  • 时间抗锯齿 (TAA):我们已经基本实现了 TAA,它使用运动向量和时间来产生一种非常流行的屏幕空间抗锯齿效果。
  • 屏幕空间环境光遮蔽 (SSAO):这是一种流行且相对便宜的照明技术,可以使场景看起来更加自然。它建立在深度预处理工作之上。
  • 自动渲染批处理和实例化:通过组合几何图形或使用实例化来自动减少绘制调用。这将使 Bevy 能够渲染数十万个对象,而不会出现停滞现象。我们实际上已经支持这一点,但它必须在我们的标准管道之外手动实现。这将在我们的内置渲染管道中“免费”带来批处理和实例化优势。
  • 一次性系统:通过命令以 基于推送的方式运行任意系统,并将它们存储为回调组件以实现超灵活的行为定制。
  • 更好的插件:更清晰、更标准化的工具,用于 将第三方插件适应您应用程序的独特架构,消除 它们初始化时的顺序依赖性 并定义 它们之间的依赖性
  • World 中拉出 !Send 数据:在旨在跨线程发送的结构中存储非线程安全数据给我们带来了很多麻烦。我们计划将其拉到 App 中,从而解决 多个世界 设计的第一步障碍。
  • 时间戳窗口和输入事件:正如在 #5984 中所讨论的那样,跟踪输入事件的确切时间对于确保事件排序和时间可以准确地重建至关重要。
  • 选择退出更改检测:通过 在编译或运行时关闭更改检测,提高小型组件的性能。
  • 全面的动画组合:支持非过渡动画组合(即动画的任意加权混合)。有关更完整的信息,请参阅 RFC

查看 Bevy 0.11 里程碑,了解当前正在考虑用于 Bevy 0.11 的工作的最新列表。

支持 Bevy #

赞助有助于使我们对 Bevy 的工作可持续发展。如果您相信 Bevy 的使命,请考虑 赞助我们... 每一点帮助都有意义!

捐赠 爱心图标

贡献者 #

Bevy 是由 一大群人 制作的。衷心感谢 173 位贡献者使此版本(和相关文档)成为可能!按随机顺序排列

  • @killercup
  • @torsteingrindvik
  • @utilForever
  • @garychia
  • @lewiszlw
  • @myreprise1
  • @tomaspecl
  • @jinleili
  • @nicopap
  • @edgarssilva
  • @aevyrie
  • @laundmo
  • @AxiomaticSemantics
  • @polygon
  • @SkiFire13
  • @SludgePhD
  • @abnormalbrain
  • @Testare
  • @ldubos
  • @SpeedRoll
  • @rodolphito
  • @hymm
  • @rdbo
  • @AndrewB330
  • @13ros27
  • @lupan
  • @iwek7
  • @ErickMVdO
  • @kerkmann
  • @davidhof
  • @Pietrek14
  • @Guvante
  • @lidong63
  • @Tirthnp
  • @x-52
  • @Suficio
  • @pascualex
  • @xgbwei
  • @BoxyUwU
  • @superdump
  • @TheRawMeatball
  • @wackbyte
  • @StarLederer
  • @MrGunflame
  • @akimakinai
  • @doup
  • @komadori
  • @darthdeus
  • @phuocthanhdo
  • @DanielJin21
  • @LiamGallagher737
  • @oliviacrain
  • @IceSentry
  • @Vrixyz
  • @johanhelsing
  • @Dessix
  • @woodroww
  • @SDesya74
  • @alphastrata
  • @wyhaya
  • @foxzool
  • @DasLixou
  • @nakedible
  • @soqb
  • @Dorumin
  • @maniwani
  • @Elabajaba
  • @geieredgar
  • @stephenmartindale
  • @TimJentzsch
  • @holyfight6
  • @targrub
  • @smessmer
  • @redwarp
  • @LoopyAshy
  • @mareq
  • @bjrnt
  • @slyedoc
  • @kurtkuehnert
  • @Charles Bournhonesque
  • @cryscan
  • @A-Walrus
  • @JMS55
  • @cBournhonesque
  • @SpecificProtagonist
  • @Shatur
  • @VitalyAnkh
  • @aktaboot
  • @dis-da-moe
  • @chrisjuchem
  • @wilk10
  • @2ne1ugly
  • @zeroacez
  • @jabuwu
  • @Aceeri
  • @coreh
  • @SuperSodaSea
  • @DGriffin91
  • @DanielHZhang
  • @mnmaita
  • @elbertronnie
  • @Zeenobit
  • @oCaioOliveira
  • @Sjael
  • @JonahPlusPlus
  • @devmitch
  • @alice-i-cecile
  • @remiCzn
  • @Sasy00
  • @sQu1rr
  • @Ptipiak
  • @zardini123
  • @alradish
  • @adam-shih
  • @LinusKall
  • @jakobhellermann
  • @Andrii Borziak
  • @figsoda
  • @james7132
  • @l1npengtul
  • @danchia
  • @AjaxGb
  • @VVishion
  • @CatThingy
  • @zxygentoo
  • @nfagerlund
  • @silvestrpredko
  • @ameknite
  • @shuoli84
  • @CrystaLamb
  • @Nanox19435
  • @james-j-obrien
  • @mockersf
  • @R2Boyo25
  • @NeoRaider
  • @MrGVSV
  • @GuillaumeGomez
  • @wangling12
  • @AndrewJakubowicz
  • @rick68
  • @RedMachete
  • @tbillington
  • @ndarilek
  • @Ian-Yy
  • @Edwox
  • @DevinLeamy
  • @TehPers
  • @cart
  • @mvlabat
  • @NiklasEi
  • @ItsDoot
  • @JayPavlina
  • @ickk
  • @Molot2032
  • @devil-ira
  • @inodentry
  • @MinerSebas
  • @JoJoJet
  • @Neo-Zhixing
  • @rparrett
  • @djeedai
  • @Pixelstormer
  • @iiYese
  • @harudagondi
  • @1e1001
  • @ickshonpe
  • @rezural
  • @arewerage
  • @ld000
  • @imustend
  • @robtfm
  • @frewsxcv

完整变更日志 #

新增 #

更改 #

删除了 #

修复了 #