#ecs #entity #component #edict #relation #query #system

不依赖std edict-proc-lib

强大的实体-组件-系统库

13 个版本 (4 个重大更新)

1.0.0-rc62024年7月16日
1.0.0-rc22024年6月21日
0.6.0 2024年6月7日
0.5.0 2023年5月10日
0.2.0-rc.32022年9月7日

#792 in 游戏开发

Download history 3/week @ 2024-04-26 1/week @ 2024-05-03 6/week @ 2024-05-17 2/week @ 2024-05-24 3/week @ 2024-05-31 159/week @ 2024-06-07 6/week @ 2024-06-14 183/week @ 2024-06-21 2/week @ 2024-06-28 215/week @ 2024-07-05 179/week @ 2024-07-12 42/week @ 2024-07-19 40/week @ 2024-07-26 4/week @ 2024-08-02

每月下载量 94
3 个包中使用 (通过 edict-proc)

MIT/Apache

46KB
565

crates docs actions MIT/Apache loc 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 - 1IdRange,这使得默认ID分配非常快速。可以为WorldBuilder提供自定义的IdRangeAllocator以使用自定义ID范围。

例如,在客户端-服务器架构中,服务器和客户端可能使用不重叠的ID范围。这样可以在不进行ID映射的情况下将服务器上序列化的状态转移到客户端,这在组件引用实体时可能很麻烦。

在多服务器或P2P架构中,IdRangeAllocator需要与每个服务器通信以分配不重叠的ID范围。

符合人体工程学的实体类型

使用ECS可能会导致大量的.unwrap()调用或过多的错误处理。有很多情况下实体是保证存在的(例如它刚刚从一个视图中返回)。为了避免在实体不可达时处理NoSuchEntity错误,Edict提供了扩展Entity特质的AliveEntity特质。各种方法需要AliveEntity处理程序,并跳过存在性检查。

为多个实体类型实现了EntityAliveEntity特质。

EntityId仅实现了Entity,因为它不提供任何保证。

EntityBound保证是活动的,允许在不需要处理实体不存在情况的方法中使用它。它保持World借用生命周期,使得不可能在世界中淘汰任何实体。使用错误的World可能导致panic。可以从关系查询中获取EntityBound

EntityLoc不仅保证实体存在,还包含实体在模板中的位置,允许在访问其组件时跳过查找步骤。与EntityBound类似,它保持World借用生命周期,使得不可能在世界中淘汰任何实体。使用错误的World可能导致panic。Entities查询可以获取EntityLoc

EntityRef是特殊的。它没有实现EntityAliveEntity特质,因为它不应该在world方法中使用。相反,它提供了对实体数据的直接访问,并允许插入/删除组件等突变操作。

组件 🛠️

非线程安全类型

支持带有一些限制的!Send!Sync组件和资源。

World本身不是可发送的,但可以在线程之间共享。拥有World的线程在文档中被称为“主线程”。

只有来自“主线程”的!Send的组件和资源才能以可变方式检索。只有来自“主线程”的!Sync的组件和资源才能以不可变方式检索。由于World的引用可能存在于“主线程”之外,应使用WorldLocal引用,它可以使用对World的可变引用创建。

带有特质的组件和无特质的组件

可选的Component特质允许在组件第一次插入时隐式注册组件类型。隐式注册使用Component实现定义的行为。当需要时,可以使用WorldBuilder显式注册以覆盖组件行为。

Component类型需要显式注册,并使用带有_external后缀的少量方法代替正常的那些。当World已经构建时,只能进行默认注册。当需要时,可以使用WorldBuilder显式注册以覆盖组件行为。

实体关系 🔗

可以向一对实体添加关系,将它们绑定在一起。查询可以检索关系并通过实体与其他实体之间的关系来过滤实体。当两个实体中的任何一个被销毁时,关系将被丢弃。Relation类型可以进一步配置被绑定实体的行为。

查询 🔍

强大的Query机制可以按组件、关系和其他标准过滤实体并检索实体数据。查询可以是可变的或不可变的、可发送的或不可发送的、有状态的或无状态的。

使用对 World 的查询可以创建视图。视图可以用来迭代匹配查询的实体,从而产生查询项。或者获取单个实体的数据。

ViewRefViewMut 是方便的类型别名,用于表示由 World 方法返回的视图类型。

运行时和编译时检查

运行时检查可用于查询可变别名避免。

ViewRefViewCell 进行运行时检查,允许存在多个具有别名访问的视图,并将检查推迟到运行时,从而防止无效的别名发生。

当不需要时,应使用带有编译时检查的 ViewMutView

当预期 View 时,ViewRefViewCell 可以锁定以创建一个 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 的实现

  • ViewViewCell 用于遍历实体及其组件。除非需要处理系统内部的视图冲突,否则请使用 View
  • 使用 ResResMut 访问资源。
  • 使用 ResLocalResMutLocal 访问非线程安全的资源。这将使系统不可发送,并强制它在主线程上运行。
  • 使用 ActionEncoder 记录变更 World 状态的动作,如实体生成、插入和删除组件或资源。
  • 使用 State 在运行之间存储系统的本地状态。

轻松的调度器 📅

Scheduler 用于运行 System。添加到 Scheduler 的系统尽可能并行运行,但它们的行为 好像 是按照添加的顺序依次执行。

如果系统不冲突,它们可以并行执行。

如果系统冲突,先添加的系统将先于后添加的系统执行。

可以使用 std 线程或 rayon 作为执行器。用户可以通过实现 ScopedExecutor 接口来提供自定义执行器。

需要 "scheduler" 功能,该功能默认启用。

钩子 🎣

当组件被替换或丢弃时,组件替换/丢弃钩子会自动调用。

当组件被注册时,它可以配备在组件值替换或丢弃时调用的钩子。隐式注册 Component 类型将注册在特质实现上定义的钩子。

当通过 World::drop 或实体被销毁时,将调用丢弃钩子,而组件从实体中移除时不会调用。

当组件被替换时,例如组件被插入到实体中且实体已经具有相同类型的组件时,将调用替换钩子。替换钩子返回一个布尔值,表示是否应调用替换组件的丢弃钩子。

钩子可以将操作记录到提供的 LocalActionEncoder 中,该操作将在引发钩子的 World 方法返回之前执行。

当组件实现 Component 特质时,特质实现上定义的钩子将自动注册以调用 Component::on_dropComponent::on_replace 方法。它们可以使用 WorldBuilder 覆盖为自定义钩子。对于非 Component 类型,钩子只能通过 WorldBuilder 注册。使用 World 的默认注册不会注册任何钩子。

Async-await ⏳

用于运行需要等待特定条件或事件或跨越多个tick的逻辑的异步执行器。

需要等待的逻辑使用系统实现可能很复杂。系统在循环中运行,通常处理具有某些组件的每个实体。实现等待逻辑需要向现有或新组件添加等待状态,而逻辑将分散在许多系统运行甚至许多系统中。

异步可以使用 await 语法等待特定条件或事件。可以访问ECS数据的异步称为“flows”。

流可以在 World 中通过使用 World::spawn_flowFlowWorld::spawn_flow 方法来生成。使用 Flows 类型作为执行器来运行生成的流。

流可以绑定到实体并使用 World::spawn_flow_forFlowWorld::spawn_flow_forEntityRef::spawn_flowFlowEntity::spawn_flow 方法来生成。如果实体被销毁,此类流将被取消。

返回 futures 的函数可以作为流。对于 World::spawn_flow 使用具有签名 FnOnce(FlowWorld) -> Future 的函数或闭包。对于 World::spawn_flow_for 使用具有签名 FnOnce(FlowEntity) -> Future 的函数或闭包。

用户可以使用 FlowWorldFlowEntitypoll* 方法来实现低级 futures,以访问任务 Context。Edict 提供了仅有的几个低级 futures,将执行等待。

WakeOnDrop 组件可以在销毁实体时唤醒任务时使用。

建议使用流来处理跨越多个时钟周期的复杂逻辑,使用系统来处理每个时钟周期运行的底层逻辑。流可以通过向实体添加特殊组件来请求系统执行操作。系统可以生成流以执行长时间运行的操作。

需要 "flow" 功能,该功能默认启用。

支持 no_std

Edict 可以在 no_std 环境中使用,但需要 alloc crate。默认情况下会启用 "std" 功能。

如果未启用 "std" 功能,错误类型将不会实现 std::error::Error

当启用 "flow" 功能但未启用 "std" 时,将使用外部函数实现 TLS。应用程序必须为这些函数提供实现,否则链接将失败。

当启用 "scheduler" 功能但未启用 "std" 时,将使用外部函数实现线程挂起。应用程序必须为这些函数提供实现,否则链接将失败。

许可证

根据您选择以下任意一项许可证授权:

由您选择。

贡献

除非您明确声明,否则根据 Apache-2.0 许可证定义的,您提交给作品以包含在内的任何有意贡献,将按照上述方式双许可,不附加任何额外条款或条件。

依赖项

约 340–790KB
~19K SLoC