#ecs #async #gamedev

apecs

异步并行实体组件系统

32次发布

0.8.3 2024年8月18日
0.8.2 2024年6月9日
0.8.1 2024年3月31日
0.7.0 2023年3月11日
0.1.0 2022年3月14日

#43 in 游戏开发

Download history 4/week @ 2024-05-31 194/week @ 2024-06-07 16/week @ 2024-06-14 5/week @ 2024-06-21 96/week @ 2024-07-05 13/week @ 2024-07-12 126/week @ 2024-08-16

每月 126 下载

MIT/Apache

140KB
2.5K SLoC

apecs

Async-friendly 和 Pleasant Entity Component System

apecs 是一个用 Rust 编写的实体组件系统,可以与在任何异步运行时中运行的 futures 共享世界资源。这使得它非常适合通用应用程序、快速游戏原型、DIY 引擎以及任何具有离散时间步骤的模拟。

原因

大多数 ECS 库(以及一般的游戏主循环)都是基于轮询的。这对于某些任务来说很好,但当在时间域中进行编程时,事情会变得复杂。Async / await 对于在时间域中进行编程非常出色,无需显式创建新线程或阻塞,但它不被 ECS 库支持。

apecs 的设计是为了与 async / await 一起使用。

是什么以及如何实现

在其核心,apecs 是一个用于在异构的轮询和异步循环之间共享资源的库。它使用可派生特性和通道来编排系统对资源的访问,并使用 rayon(如果可用)来实现并发。

目标

  • 生产力
  • 灵活性
  • 可观察性
  • 非常全面且具有竞争力的性能,与灵感的 ECS 库相媲美
    • specsbevy_ecshecslegionshipyardplanck_ecs
    • 由 criterion 基准测试支持

功能

以下是一个与其他 ECS 相比的功能快速表格。

功能 apecs bevy_ecs hecs legion planck_ecs shipyard specs
storage archetypal hybrid archetypal archetypal separated sparse separated
系统调度 ✔️ ✔️ ✔️ ✔️ ✔️ ✔️
早期退出系统 ✔️
并行系统 ✔️ ✔️ ✔️ ✔️ ✔️ ✔️
变更跟踪 ✔️ ✔️ kinda ✔️ ✔️
异步支持 ✔️

功能示例

  • 具有早期退出和失败的系统
  use apecs::*;

  #[derive(Clone, Copy, Debug, Default, PartialEq)]
  struct Number(u32);

  fn demo_system(mut u32_number: ViewMut<Number>) -> Result<(), GraphError> {
      u32_number.0 += 1;
      if u32_number.0 == 3 {
          end()
      } else {
          ok()
      }
  }

  let mut world = World::default();
  world.add_subgraph(graph!(demo_system));
  world.run().unwrap();
  assert_eq!(Number(3), *world.get_resource::<Number>().unwrap());
  • 异步支持
    • 使用闭包通过 Facade 访问世界资源
    • 资源无需生命周期即可获取
    • 与任何异步运行时兼容
  use apecs::*;

  #[derive(Clone, Copy, Debug, Default, PartialEq)]
  struct Number(u32);

  let mut world = World::default();
  let mut facade = world.facade();

  let task = smol::spawn(async move {
      loop {
          let i = facade
              .visit(|mut u32_number: ViewMut<Number>| {
                  u32_number.0 += 1;
                  u32_number.0
              })
              .await
              .unwrap();
          if i > 5 {
              break;
          }
      }
  });

  while !task.is_finished() {
      world.tick().unwrap();
      world.get_facade_schedule().unwrap().run().unwrap();
  }

  assert_eq!(Number(6), *world.get_resource::<Number>().unwrap());
  • 系统数据派生宏
  use apecs::*;

  #[derive(Edges)]
  struct MyData {
      entities: View<Entities>,
      u32_number: ViewMut<u32>,
  }

  let mut world = World::default();
  world
      .visit(|mut my_data: MyData| {
          *my_data.u32_number = 1;
      })
      .unwrap();
  • 系统调度
    • 兼容的系统放置在并行批次中(批次是一组可以并行运行的系统,即它们没有冲突的借用)
    • 系统可以依赖其他系统在运行之前或之后运行
    • 屏障
      use apecs::*;
    
      fn one(mut u32_number: ViewMut<u32>) -> Result<(), GraphError> {
          *u32_number += 1;
          end()
      }
    
      fn two(mut u32_number: ViewMut<u32>) -> Result<(), GraphError> {
          *u32_number += 1;
          end()
      }
    
      fn exit_on_three(mut f32_number: ViewMut<f32>) -> Result<(), GraphError> {
          *f32_number += 1.0;
          if *f32_number == 3.0 {
              end()
          } else {
              ok()
          }
      }
    
      fn lastly((u32_number, f32_number): (View<u32>, View<f32>)) -> Result<(), GraphError> {
          if *u32_number == 2 && *f32_number == 3.0 {
              end()
          } else {
              ok()
          }
      }
    
      let mut world = World::default();
      world.add_subgraph(
          graph!(
              // one should run before two
              one < two,
              // exit_on_three has no dependencies
              exit_on_three
          )
          // add a barrier
          .with_barrier()
          .with_subgraph(
              // all systems after a barrier run after the systems before a barrier
              graph!(lastly),
          ),
      );
    
      assert_eq!(
          vec![vec!["exit_on_three", "one"], vec!["two"], vec!["lastly"]],
          world.get_schedule_names()
      );
    
      world.tick().unwrap();
    
      assert_eq!(
          vec![vec!["exit_on_three"], vec!["lastly"]],
          world.get_schedule_names()
      );
    
      world.tick().unwrap();
      world.tick().unwrap();
      assert!(world.get_schedule_names().is_empty());
    
  • 组件存储
    • 针对空间和迭代时间进行优化,类似于 archetypal
    • 具有 "maybe" 和 "without" 语义的查询
    • 可以找到单个实体而不进行迭代或过滤
    • 添加和修改时间跟踪
    • 并行查询(内部并行性)
      use apecs::*;
    
      // Make a type for tracking changes
      #[derive(Default)]
      struct MyTracker(u64);
    
      fn create(mut entities: ViewMut<Entities>) -> Result<(), GraphError> {
          for mut entity in (0..100).map(|_| entities.create()) {
              entity.insert_bundle((0.0f32, 0u32, format!("{}:0", entity.id())));
          }
          end()
      }
    
      fn progress(q_f32s: Query<&mut f32>) -> Result<(), GraphError> {
          for f32 in q_f32s.query().iter_mut() {
              **f32 += 1.0;
          }
          ok()
      }
    
      fn sync(
          (q_others, mut tracker): (Query<(&f32, &mut String, &mut u32)>, ViewMut<MyTracker>),
      ) -> Result<(), GraphError> {
          for (f32, string, u32) in q_others.query().iter_mut() {
              if f32.was_modified_since(tracker.0) {
                  **u32 = **f32 as u32;
                  **string = format!("{}:{}", f32.id(), **u32);
              }
          }
          tracker.0 = apecs::current_iteration();
          ok()
      }
    
      // Entities and Components (which stores components) are default
      // resources
      let mut world = World::default();
      world.add_subgraph(graph!(
          create < progress < sync
      ));
    
      assert_eq!(
          vec![vec!["create"], vec!["progress"], vec!["sync"]],
          world.get_schedule_names()
      );
    
      world.tick().unwrap(); // entities are created, components applied lazily
      world.tick().unwrap(); // f32s are modified, u32s and strings are synced
      world.tick().unwrap(); // f32s are modified, u32s and strings are synced
    
      world
          .visit(|q_bundle: Query<(&f32, &u32, &String)>| {
              assert_eq!(
                  (2.0f32, 2u32, "13:2".to_string()),
                  q_bundle
                      .query()
                      .find_one(13)
                      .map(|(f, u, s)| (**f, **u, s.to_string()))
                      .unwrap()
              );
          })
          .unwrap();
    
  • 外部并行性(并行运行系统)
    • 并行系统调度
    • 并行执行异步未来
    • 并行度可配置(可以是自动或请求的线程数,包括1)
    use apecs::*;

    #[derive(Default)]
    struct F32(f32);

    let mut world = World::default();

    fn one(mut f32_number: ViewMut<F32>) -> Result<(), GraphError> {
        f32_number.0 += 1.0;
        ok()
    }

    fn two(f32_number: View<F32>) -> Result<(), GraphError> {
        println!("system two reads {}", f32_number.0);
        ok()
    }

    fn three(f32_number: View<F32>) -> Result<(), GraphError> {
        println!("system three reads {}", f32_number.0);
        ok()
    }

    world
        .add_subgraph(graph!(one, two, three))
        .with_parallelism(Parallelism::Automatic);

    world.tick().unwrap();
  • 完全兼容WASM,并在浏览器中运行

路线图

  • 你的想法在这里

测试

cargo test
wasm-pack test --firefox crates/apecs

我喜欢Firefox,但你也可以使用不同的浏览器进行wasm测试。测试确保apecs在wasm上工作。

基准测试

apecs基准测试将其与我最喜欢的ECS库进行了比较:specsbevyhecslegionshipyardplanck_ecs

cargo bench -p benchmarks

最低支持的Rust版本1.65

apecs使用泛型关联类型为其组件迭代特质。

依赖关系

~4–9.5MB
~94K SLoC