27个版本

1.0.0-rc62024年7月16日
1.0.0-rc22024年6月21日
0.6.1 2024年6月9日
0.5.0 2023年5月10日
0.0.0 2021年12月13日

25 in 游戏开发

Download history 1/week @ 2024-04-27 108/week @ 2024-06-01 233/week @ 2024-06-08 89/week @ 2024-06-15 89/week @ 2024-06-22 2/week @ 2024-06-29 247/week @ 2024-07-06 114/week @ 2024-07-13 30/week @ 2024-07-20 66/week @ 2024-07-27 1/week @ 2024-08-03

每月105次下载
用于 2 crates

MIT/Apache

1MB
21K SLoC

crates docs actions MIT/Apache loc Edict是一个快速、强大且易用的ECS crate,扩展了传统的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

实体组件系统中,我们创建实体并将它们地址化以获取关联数据。Edict提供了EntityId类型来地址化实体。

EntityId是实体的世界唯一标识符。Edict使用ID而不生成和回收,为此它使用u64底层类型和一些空位。在连续创建ID数百年后才会耗尽。这大大简化了World状态的序列化,因为它不需要对实体ID进行任何处理。

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

实体ID按照顺序从由IdRange分配的ID范围中分配,这些ID范围由IdRangeAllocator分配。默认情况下,使用从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 特性,因为它不应在全局方法中使用。相反,它提供了对实体数据的直接访问,并允许插入/删除组件等修改。

组件 🛠️

非线程安全类型

支持一些限制的 !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 配置,这是它应该从其中借用请求类型的组件。如果组件不提供请求的类型,则引发恐慌。跳过没有组件的实体。

资源 📦

内置类型映射 "resources" 用于单例值。资源可以插入到/从 World 中提取。资源独立于实体及其组件存在。

动作 🏃‍♂️

使用 ActionEncoder 记录动作,稍后使用对 World 的可变访问运行它们。或者,当动作不是 Send 时,使用 LocalActionEncoder。或者,使用方便的 WorldLocal::defer 方法将动作推迟到内部 LocalActionEncoder

自动更改跟踪 🤖

每个组件实例都配备了一个纪元计数器,用于跟踪组件的最后潜在突变。查询可以读取和更新组件的纪元以跟踪变化。提供带有Modified类型的查询来过滤最近更改的组件。可以通过World::epoch方法获取最后纪元。

系统 ⚙️

系统是一个方便的方式来构建在World上操作的逻辑。Edict定义了System特质以在World上运行逻辑。并为可转换为SystemIntoSystem特质。

函数可以自动实现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的逻辑的future执行器。

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

future可以使用await语法等待特定条件或事件。可以访问ECS数据的future在Edict中称为"flows"。

可以使用WorldWorld::spawn_flowFlowWorld::spawn_flow方法在Flows中生成flows。用作执行器来运行生成的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只提供了一两个低级未来,将进行等待

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

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

需要"flow"特性,该特性默认启用。

no_std支持

Edict可以在no_std环境中使用,但需要alloc包。默认启用"std"特性。

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

当启用"flow"特性且未启用"std"时,将使用外部函数来实现TLS。应用程序必须提供这些函数的实现,否则链接将失败。

当“调度器”功能启用且“std”未启用时,外部函数用于实现线程挂起。应用程序必须为这些函数提供实现,否则链接将失败。

许可证

在以下任一许可证下授权:

任选其一。

贡献

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

依赖项

~2.2–8MB
~60K SLoC