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 游戏开发
每月 126 下载
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 库相媲美
- 如
specs
、bevy_ecs
、hecs
、legion
、shipyard
、planck_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库进行了比较:specs
,bevy
,hecs
,legion
,shipyard
和planck_ecs
。
cargo bench -p benchmarks
最低支持的Rust版本1.65
apecs
使用泛型关联类型为其组件迭代特质。
依赖关系
~4–9.5MB
~94K SLoC