13 个版本 (4 个重大更新)
| 1.0.0-rc6 | 2024年7月16日 |
|---|---|
| 1.0.0-rc2 | 2024年6月21日 |
| 0.6.0 | 2024年6月7日 |
| 0.5.0 | 2023年5月10日 |
| 0.2.0-rc.3 | 2022年9月7日 |
#792 in 游戏开发
每月下载量 94
在 3 个包中使用 (通过 edict-proc)
46KB
565 行
Edict是一个快速、强大且高效的ECS crate,扩展了传统的ECS功能集。由🦀编写
基本用法 🌱
use edict::prelude::*;
// Create world instance.
let mut world = World::new();
// Declare some components.
#[derive(Component)]
struct Pos(f32, f32);
// Declare some more.
#[derive(Component)]
struct Vel(f32, f32);
// Spawn entity with components.
world.spawn((Pos(0.0, 0.0), Vel(1.0, 1.0)));
// Query components and iterate over views.
for (pos, vel) in world.view::<(&mut Pos, &Vel)>() {
pos.0 += vel.0;
pos.1 += vel.1;
}
// Define functions that will be used as systems.
#[edict::system::system] // This attribute is optional, but it catches if function is not a system.
fn move_system(pos_vel: View<(&mut Pos, &Vel)>) {
for (pos, vel) in pos_vel {
pos.0 += vel.0;
pos.1 += vel.1;
}
}
// Create scheduler to run systems. Requires "scheduler" feature.
use edict::scheduler::Scheduler;
let mut scheduler = Scheduler::new();
scheduler.add_system(move_system);
// Run systems without parallelism.
scheduler.run_sequential(&mut world);
// Run systems using threads. Requires "std" feature.
scheduler.run_threaded(&mut world);
// Or use custom thread pool.
功能
实体 🧩
简单ID
在 实体 组件系统中,我们创建实体并将它们关联起来以获取相关数据。Edict提供EntityId类型来标识实体。
EntityId作为一个实体的全局唯一标识符。Edict使用ID而不进行生成和回收,为此它使用了几个特例的u64底层类型。在数百年的时间里不断地创建ID,直到耗尽。这极大地简化了World状态的序列化,因为它不需要对实体ID进行任何处理。
默认情况下,实体ID只在单个World中是唯一的。对于多世界场景,Edict提供了一种方法,可以在任何所需组合的世界之间使实体ID唯一。
实体ID按照顺序从由IdRange分配的ID范围内分配,而IdRangeAllocator负责分配这些ID范围。默认情况下,使用从1到u64::MAX - 1的IdRange,这使得默认ID分配非常快速。可以为WorldBuilder提供自定义的IdRangeAllocator以使用自定义ID范围。
例如,在客户端-服务器架构中,服务器和客户端可能使用不重叠的ID范围。这样可以在不进行ID映射的情况下将服务器上序列化的状态转移到客户端,这在组件引用实体时可能很麻烦。
在多服务器或P2P架构中,IdRangeAllocator需要与每个服务器通信以分配不重叠的ID范围。
符合人体工程学的实体类型
使用ECS可能会导致大量的.unwrap()调用或过多的错误处理。有很多情况下实体是保证存在的(例如它刚刚从一个视图中返回)。为了避免在实体不可达时处理NoSuchEntity错误,Edict提供了扩展Entity特质的AliveEntity特质。各种方法需要AliveEntity处理程序,并跳过存在性检查。
为多个实体类型实现了Entity和AliveEntity特质。
EntityId仅实现了Entity,因为它不提供任何保证。
EntityBound保证是活动的,允许在不需要处理实体不存在情况的方法中使用它。它保持World借用生命周期,使得不可能在世界中淘汰任何实体。使用错误的World可能导致panic。可以从关系查询中获取EntityBound。
EntityLoc不仅保证实体存在,还包含实体在模板中的位置,允许在访问其组件时跳过查找步骤。与EntityBound类似,它保持World借用生命周期,使得不可能在世界中淘汰任何实体。使用错误的World可能导致panic。Entities查询可以获取EntityLoc。
EntityRef是特殊的。它没有实现Entity或AliveEntity特质,因为它不应该在world方法中使用。相反,它提供了对实体数据的直接访问,并允许插入/删除组件等突变操作。
组件 🛠️
非线程安全类型
World本身不是可发送的,但可以在线程之间共享。拥有World的线程在文档中被称为“主线程”。
只有来自“主线程”的!Send的组件和资源才能以可变方式检索。只有来自“主线程”的!Sync的组件和资源才能以不可变方式检索。由于World的引用可能存在于“主线程”之外,应使用WorldLocal引用,它可以使用对World的可变引用创建。
带有特质的组件和无特质的组件
可选的Component特质允许在组件第一次插入时隐式注册组件类型。隐式注册使用Component实现定义的行为。当需要时,可以使用WorldBuilder显式注册以覆盖组件行为。
非Component类型需要显式注册,并使用带有_external后缀的少量方法代替正常的那些。当World已经构建时,只能进行默认注册。当需要时,可以使用WorldBuilder显式注册以覆盖组件行为。
实体关系 🔗
可以向一对实体添加关系,将它们绑定在一起。查询可以检索关系并通过实体与其他实体之间的关系来过滤实体。当两个实体中的任何一个被销毁时,关系将被丢弃。Relation类型可以进一步配置被绑定实体的行为。
查询 🔍
强大的Query机制可以按组件、关系和其他标准过滤实体并检索实体数据。查询可以是可变的或不可变的、可发送的或不可发送的、有状态的或无状态的。
使用对 World 的查询可以创建视图。视图可以用来迭代匹配查询的实体,从而产生查询项。或者获取单个实体的数据。
ViewRef 和 ViewMut 是方便的类型别名,用于表示由 World 方法返回的视图类型。
运行时和编译时检查
运行时检查可用于查询可变别名避免。
ViewRef 和 ViewCell 进行运行时检查,允许存在多个具有别名访问的视图,并将检查推迟到运行时,从而防止无效的别名发生。
当不需要时,应使用带有编译时检查的 ViewMut 和 View。
当预期 View 时,ViewRef 和 ViewCell 可以锁定以创建一个 View。
借用
组件类型可以定义借用操作,以从它借用其他类型。借用类型可能不是有尺寸的,允许借用切片和 dyn 特性。提供了一个宏来帮助定义借用操作。提供了尝试从合适的组件中借用类型的查询。
BorrowAll从实现所需借用类型的所有组件中借用。返回一个包含借用值的Vec,因为实体的多个组件可能提供它。如果没有任何组件提供所需类型,则跳过实体。BorrowAny从第一个实现所需借用类型的合适组件中借用。返回一个单一值。如果没有任何组件提供所需类型,则跳过实体。BorrowOne配置为从它应借用的组件的TypeId。如果组件不提供所需类型,则引发恐慌。跳过没有组件的实体。
资源 📦
名为“资源”的单例值的内置类型映射。资源可以插入到/从 World 中检索。资源独立于实体及其组件存在。
动作 🏃♂️
使用 ActionEncoder 记录动作,并可以随后以可变访问的方式运行 World。或者,当动作不是 Send 时,使用 LocalActionEncoder。或者使用方便的 WorldLocal::defer* 方法将动作推迟到内部 LocalActionEncoder。
自动变更跟踪 🤖
每个组件实例都配备了一个纪元计数器,用于跟踪组件的最后潜在变更。查询可以读取和更新组件的纪元来跟踪变更。提供 Modified 类型来过滤最近变更的组件。可以通过 World::epoch 方法获得最后的纪元。
系统 ⚙️
系统是构建在 World 上操作的逻辑的便捷方式。Edict 定义了 System 特征,用于在 World 上运行逻辑。还有 IntoSystem 特征,用于将类型转换为 System。
函数可以自动实现 IntoSystem - 它需要返回 () 并接受实现 FnArg 特征的参数。存在 FnArg 的实现
View和ViewCell用于遍历实体及其组件。除非需要处理系统内部的视图冲突,否则请使用View。- 使用
Res和ResMut访问资源。 - 使用
ResLocal和ResMutLocal访问非线程安全的资源。这将使系统不可发送,并强制它在主线程上运行。 - 使用
ActionEncoder记录变更World状态的动作,如实体生成、插入和删除组件或资源。 - 使用
State在运行之间存储系统的本地状态。
轻松的调度器 📅
Scheduler 用于运行 System。添加到 Scheduler 的系统尽可能并行运行,但它们的行为 好像 是按照添加的顺序依次执行。
如果系统不冲突,它们可以并行执行。
如果系统冲突,先添加的系统将先于后添加的系统执行。
可以使用 std 线程或 rayon 作为执行器。用户可以通过实现 ScopedExecutor 接口来提供自定义执行器。
需要 "scheduler" 功能,该功能默认启用。
钩子 🎣
当组件被替换或丢弃时,组件替换/丢弃钩子会自动调用。
当组件被注册时,它可以配备在组件值替换或丢弃时调用的钩子。隐式注册 Component 类型将注册在特质实现上定义的钩子。
当通过 World::drop 或实体被销毁时,将调用丢弃钩子,而组件从实体中移除时不会调用。
当组件被替换时,例如组件被插入到实体中且实体已经具有相同类型的组件时,将调用替换钩子。替换钩子返回一个布尔值,表示是否应调用替换组件的丢弃钩子。
钩子可以将操作记录到提供的 LocalActionEncoder 中,该操作将在引发钩子的 World 方法返回之前执行。
当组件实现 Component 特质时,特质实现上定义的钩子将自动注册以调用 Component::on_drop 和 Component::on_replace 方法。它们可以使用 WorldBuilder 覆盖为自定义钩子。对于非 Component 类型,钩子只能通过 WorldBuilder 注册。使用 World 的默认注册不会注册任何钩子。
Async-await ⏳
用于运行需要等待特定条件或事件或跨越多个tick的逻辑的异步执行器。
需要等待的逻辑使用系统实现可能很复杂。系统在循环中运行,通常处理具有某些组件的每个实体。实现等待逻辑需要向现有或新组件添加等待状态,而逻辑将分散在许多系统运行甚至许多系统中。
异步可以使用 await 语法等待特定条件或事件。可以访问ECS数据的异步称为“flows”。
流可以在 World 中通过使用 World::spawn_flow 或 FlowWorld::spawn_flow 方法来生成。使用 Flows 类型作为执行器来运行生成的流。
流可以绑定到实体并使用 World::spawn_flow_for、FlowWorld::spawn_flow_for、EntityRef::spawn_flow 或 FlowEntity::spawn_flow 方法来生成。如果实体被销毁,此类流将被取消。
返回 futures 的函数可以作为流。对于 World::spawn_flow 使用具有签名 FnOnce(FlowWorld) -> Future 的函数或闭包。对于 World::spawn_flow_for 使用具有签名 FnOnce(FlowEntity) -> Future 的函数或闭包。
用户可以使用 FlowWorld 和 FlowEntity 的 poll* 方法来实现低级 futures,以访问任务 Context。Edict 提供了仅有的几个低级 futures,将执行等待。
yield_now!一次将控制权交给执行器,并在下一次执行时继续。FlowEntity::wait_despawned等待实体被销毁。FlowEntity::wait_has_component等待实体获得一个组件。
WakeOnDrop 组件可以在销毁实体时唤醒任务时使用。
建议使用流来处理跨越多个时钟周期的复杂逻辑,使用系统来处理每个时钟周期运行的底层逻辑。流可以通过向实体添加特殊组件来请求系统执行操作。系统可以生成流以执行长时间运行的操作。
需要 "flow" 功能,该功能默认启用。
支持 no_std
Edict 可以在 no_std 环境中使用,但需要 alloc crate。默认情况下会启用 "std" 功能。
如果未启用 "std" 功能,错误类型将不会实现 std::error::Error。
当启用 "flow" 功能但未启用 "std" 时,将使用外部函数实现 TLS。应用程序必须为这些函数提供实现,否则链接将失败。
当启用 "scheduler" 功能但未启用 "std" 时,将使用外部函数实现线程挂起。应用程序必须为这些函数提供实现,否则链接将失败。
许可证
根据您选择以下任意一项许可证授权:
- Apache 许可证 2.0 版本,(license/APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT 许可证 (license/MIT 或 https://open-source.org.cn/licenses/MIT)
由您选择。
贡献
除非您明确声明,否则根据 Apache-2.0 许可证定义的,您提交给作品以包含在内的任何有意贡献,将按照上述方式双许可,不附加任何额外条款或条件。
依赖项
约 340–790KB
~19K SLoC