#index #bevy #indices #game-engine

bevy_mod_index

允许在游戏引擎Bevy中通过索引值高效查询组件

6个版本 (重大变更)

0.5.0 2024年7月4日
0.4.1 2024年4月20日
0.4.0 2024年2月18日
0.3.0 2023年11月5日
0.1.0 2023年3月6日

游戏开发中排名122

每月下载42
3个库中使用(直接使用2个)

MIT协议

43KB
603

bevy_mod_index

这是一个Rust库,允许在游戏引擎Bevy中通过组件值高效查询组件。

兼容性

Bevy版本 bevy_mod_index版本
0.14 0.5.x
0.13 0.4.x
0.12 0.3.0
0.11 0.2.0
0.10 0.1.0

特性

特性名称 描述
reflect 向存储资源添加reflect派生。

使用案例

通常希望在系统中编写只操作具有特定值的组件的代码,例如。

fn move_living_players(mut players: Query<&mut Transform, &Player>) {
  for (mut transform, player) in &players {
    if player.is_alive() {
      move_player(transform);
    }
  }
}

有了索引,我们可以将代码改为

fn move_living_players(
  mut transforms: Query<&mut Transform>, 
  player_alive_idx: Index<PlayerAlive>
) {
  for entity in &player_alive_idx.get(true) {
      transforms.get(entity).unwrap().move_player(transform);
  }
}

在某些情况下,这样的改变可能是有益的

  • 如果is_alive的计算成本很高,索引可以通过缓存结果并在数据实际更改时重新计算来节省工作。
    • 如果用于计算结果的组件数据不经常更改,我们可以跨帧使用缓存的值。
    • 如果组件只在帧的开始部分更改,并且需要在之后多次使用结果,我们可以跨不同的系统使用缓存的值(甚至如果是多次计算的同一系统)。
  • 如果我们不太关心性能,索引可以提供更友好的API。

不过,索引会增加一定的开销,引入它们可能会使您的系统变慢。如果您关心性能,请在引入索引前后对系统进行性能分析。

入门指南

首先,导入预览。

use bevy_mod_index::prelude::*;

接下来,实现IndexInfo特质。如果您的组件只需要一个索引,您可以直接在组件上实现此特质。如果您需要多个索引,您可以使用简单的单元结构为第一个索引之后的每个索引。您还可以使用单元结构提供更描述性的名称,即使您只需要一个索引。

您必须指定

  • 要索引的组件类型,
  • 您想用于查找的值类型,
  • 为组件计算该值的函数,
  • 如何存储实体与其相应组件计算出的值之间的关系,以及
  • 索引何时刷新其最新数据。
struct NearOrigin {}
impl IndexInfo for NearOrigin {
  type Component = Transform;
  type Value = bool;
  type Storage = HashmapStorage<Self>;
  type REFRESH_POLICY = IndexRefreshPolicy::WhenRun;

  fn value(t: &Transform) -> bool {
    t.translation.length() < 5.0
  }
}

最后,将Index系统参数包含在您的系统中,并使用它来查询实体!

fn count_players_and_enemies_near_spawn(
  players: Query<(), With<(&Player, &Transform)>>,
  enemies: Query<(), With<(&Enemy, &Transform)>>,
  index: Index<NearOrigin>,
) {
  let (mut player_count, mut enemy_count) = (0, 0);
  
  let entities_near_spawn: HashSet<Entity> = index.lookup(true);
  for entity in entities_near_spawn.into_iter() {
    if let Ok(()) = players.get(entity) {
      player_count += 1;
    }
    if let Ok(()) = enemies.get(entity) {
      enemy_count += 1;
    }
  }
  
  println!("There are {} players and {} enemies near spawn!", player_count, enemy_count)
}

存储实现

HashmapStorage 使用一个 Resource 来缓存 Entity 和从其组件计算得出的值之间的映射。它使用一个自定义的 SystemParam 来获取它需要的数据,以便在需要时更新自身。这是一个很好的默认选择,尤其是在预期的 lookup 返回的 Entity 数量只是整个查询中的一小部分时。

NoStorage,正如其名所示,不存储任何索引数据。相反,每次查询时它都会遍历所有数据,为每个组件计算 value 函数,就像上面第一个 move_living_players 示例一样。这个选项允许你使用索引 API 而不必承担与 HashmapStorage 一样多的开销(尽管仍然比直接遍历所有组件要高)。

刷新策略

使用 HashmapStorage 的索引必须定期进行 refresh,以便能够准确反映组件被添加、更改和删除的状态。指定 IndexRefreshPolicy 配置将使索引能够自动使用几种不同的时机之一来刷新。

IndexRefreshPolicy::WhenRun 如果你不确定要使用哪种刷新策略,是一个很好的默认选择,但其他策略可以在 文档中找到

反射

可以通过选择可选的 reflect crate 功能来启用存储资源的反射。这对于使用 bevy-inspector-egui 检查底层存储非常有用。

为了使资源出现在检查器中,你需要为每个索引手动注册存储,例如 app.register_type::<HashmapStorage<NearOrigin>>(); 确保你的 IndexInfo 类型及其相关组件/值也派生了 Reflect

注意:你不应该依赖于这些资源的内部结构,因为它们可能会在不同的版本之间发生变化。

API 稳定性

考虑到我在尝试哪些名称和模式最自然、最富有表现力时,API 是极度不稳定的,并且也在支持新功能。

性能

我还没有在优化性能索引上投入很多精力。然而,我已经进行了一些初步测试,以了解它们大约增加了多少开销。

在有 100 万个实体的情况下,尽管没有组件在帧与帧之间改变,使用组件本身作为索引值,在 ~300 个实体上操作需要

  • 比使用 NoStorage 的简单迭代慢 2-4 倍。
  • 比使用 HashmapStorage 的简单迭代慢 3-5 倍。

在相同的设置下,除了 5% 的实体每帧更新之外,HashmapStorage 的性能下降到简单迭代 30-40 倍。

我目前正在添加更多的具体基准测试,并且我确实有一些会影响性能的变化计划。

联系我

如果你对 API 的改进有建议,或者关于提高性能的想法,我很乐意听听。提交一个问题,或者在 Bevy 的 discord 上的 bevy_mod_index #crate-help 线路上发帖。

故障排除

  • 查询<(bevy_ecs::实体::Entity,&bevy_mod_index::索引::测试::数字bevy_ecs::查询::获取::ChangeTrackers<bevy_mod_index::索引::测试::数字>), ()> 系统中bevy_mod_index::索引::测试::adder_some::{{闭包}}访问组件(s) bevy_mod_index::索引::测试::数字以与先前系统参数冲突的方式.考虑使用没有<T>``来创建不相交的查询或将冲突的查询合并到``ParamSet``.
    • 索引使用其组件的只读查询来在它被使用之前更新索引。如果你有一个查询在同一系统中以可变方式访问这些组件,并且有一个Index,你可以将它们组合到一个ParamSet中。

未来工作

  • 文档
  • 在组件更改时更新索引而不是在索引使用时更新的选项。
    • 直观上,需要引擎支持自定义DerefMut钩子,但这可能会在索引未使用时增加开销。其他解决方案可能是可能的。
      • 也许有一天Component派生将接受一个属性,该属性通过指定&mut TMut<T>作为引用类型来启用/禁用更改检测,并且我们可以添加一个第三个选项,即IndexedMut<T>,它将自动在某个资源中查找组件的所有索引并将实体添加到待重新索引的列表中。
  • 除了HashMap之外的更多存储选项。
    • 有序容器以允许查询“附近”的值。
      • 1D数据应该足够简单,但也希望支持用于位置的kd树。
  • 超过一个Component的索引。
  • Component子集的索引
    • 用任意查询替换组件可能涵盖这两个情况。
  • 为简单情况的IndexInfo提供派生,其中组件本身用作值。

依赖关系

~22MB
~418K SLoC