#ecs #entity #component #edict #relation #system #check

无std edict-proc

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

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日

#2291 in 游戏开发

Download history 1/week @ 2024-04-26 4/week @ 2024-05-17 1/week @ 2024-05-24 1/week @ 2024-05-31 155/week @ 2024-06-07 4/week @ 2024-06-14 182/week @ 2024-06-21 3/week @ 2024-06-28 200/week @ 2024-07-05 175/week @ 2024-07-12 43/week @ 2024-07-19 50/week @ 2024-07-26 2/week @ 2024-08-02

每月102次下载
2 个包中使用 (通过 edict)

MIT/Apache

71KB
592

crates docs actions MIT/Apache loc Edict 是一个快速、强大且高效的ECS包,它扩展了传统的ECS功能集。由你的同僚 🦀用Rust编写

基本用法 🌱

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

Entity 组件系统中,我们创建实体并将它们用于获取相关数据。Edict提供了 EntityId 类型来标识实体。

EntityId 是一个实体的世界唯一标识符。Edict使用ID而不生成和回收,为此它采用 u64 作为底层类型,并有一些预留空间。只需不断创建ID数百年,就可以避免耗尽。这极大地简化了 World 状态的序列化,因为它不需要对实体ID进行处理。

默认情况下,实体ID仅在单个 World 中是唯一的。对于多世界场景,Edict提供了一种方法,可以在任何所需组合的世界之间使实体ID唯一。

标识符按顺序从由 IdRange 分配的标识符范围中分配,这些范围由 IdRangeAllocator 分配。默认情况下,使用从 1 到 u64::MAX - 1IdRange,这使得默认的标识符分配非常快速。可以为 WorldBuilder 提供自定义的 IdRangeAllocator 来使用自定义的标识符范围。

例如,在客户端-服务器架构中,服务器和客户端可能使用不重叠的标识符范围。这样可以允许在服务器上序列化的状态无标识符映射地传输到客户端,这在组件引用实体时可能很麻烦。

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

人体工学实体类型

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

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

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

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

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

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

组件 🛠️

非线程安全类型

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

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

可发送的组件和资源只能从 "主" 线程获取可变引用。可同步的组件和资源只能从 "主" 线程获取不可变引用。由于 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 的注册不会注册任何钩子。

异步-等待 ⏳

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

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

异步可以用来等待某些条件或事件。可以访问 ECS 数据的异步称为 "flows"。

可以使用 WorldWorld::spawn_flowFlowWorld::spawn_flow 方法在 Flows 中启动 "flows"。该类型用作执行器来运行启动的 "flows"。

流可以被绑定到实体,并通过使用World::spawn_flow_forFlowWorld::spawn_flow_forEntityRef::spawn_flowFlowEntity::spawn_flow方法进行生成。当实体被销毁时,此类流将被取消。

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

用户可以使用FlowWorldFlowEntitypoll方法实现低级别的future,以访问任务Context。Edict只提供了一两个低级别的future,用于执行等待操作。

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

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

需要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