27个版本
1.0.0-rc6 | 2024年7月16日 |
---|---|
1.0.0-rc2 | 2024年6月21日 |
0.6.1 | 2024年6月9日 |
0.5.0 | 2023年5月10日 |
0.0.0 | 2021年12月13日 |
25 in 游戏开发
每月105次下载
用于 2 crates
1MB
21K SLoC
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 - 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
的线程在文档中被称为“主”线程。
只有从“主”线程可以可变地获取标记为 !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
配置,这是它应该从其中借用请求类型的组件。如果组件不提供请求的类型,则引发恐慌。跳过没有组件的实体。
资源 📦
内置类型映射 "resources" 用于单例值。资源可以插入到/从 World
中提取。资源独立于实体及其组件存在。
动作 🏃♂️
使用 ActionEncoder
记录动作,稍后使用对 World
的可变访问运行它们。或者,当动作不是 Send
时,使用 LocalActionEncoder
。或者,使用方便的 WorldLocal::defer
方法将动作推迟到内部 LocalActionEncoder
。
自动更改跟踪 🤖
每个组件实例都配备了一个纪元计数器,用于跟踪组件的最后潜在突变。查询可以读取和更新组件的纪元以跟踪变化。提供带有Modified
类型的查询来过滤最近更改的组件。可以通过World::epoch
方法获取最后纪元。
系统 ⚙️
系统是一个方便的方式来构建在World
上操作的逻辑。Edict定义了System
特质以在World
上运行逻辑。并为可转换为System
的IntoSystem
特质。
函数可以自动实现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的逻辑的future执行器。
需要等待的逻辑可能很复杂,难以使用系统实现。系统在循环中运行,通常对具有某些组件的每个实体进行处理。实现等待逻辑需要向现有或新组件添加等待状态,逻辑将分散在许多系统运行甚至许多系统中。
future可以使用await
语法等待特定条件或事件。可以访问ECS数据的future在Edict中称为"flows"。
可以使用World
的World::spawn_flow
或FlowWorld::spawn_flow
方法在Flows
中生成flows。用作执行器来运行生成的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只提供了一两个低级未来,将进行等待
yield_now!
将控制权一次性地交还给执行器,并在下一次执行时恢复。FlowEntity::wait_despawned
等待实体被销毁。FlowEntity::wait_has_component
等待实体获得一个组件。
WakeOnDrop
组件可以在销毁实体时应唤醒任务时使用。
建议使用流程处理跨越多个刻(ticks)的高级逻辑,使用系统来执行每个刻运行的低级逻辑。流程可以通过向实体添加特殊组件来请求系统执行操作。系统也可以生成流程来执行长时间运行的操作。
需要"flow"
特性,该特性默认启用。
no_std支持
Edict可以在no_std
环境中使用,但需要alloc
包。默认启用"std"
特性。
如果未启用"std"特性,错误类型将不会实现std::error::Error
。
当启用"flow"特性且未启用"std"时,将使用外部函数来实现TLS。应用程序必须提供这些函数的实现,否则链接将失败。
当“调度器”功能启用且“std”未启用时,外部函数用于实现线程挂起。应用程序必须为这些函数提供实现,否则链接将失败。
许可证
在以下任一许可证下授权:
- Apache License, Version 2.0, (license/APACHE 或 http://www.apache.org/licenses/LICENSE2.0)
- MIT 许可证 (license/MIT 或 http://opensource.org/licenses/MIT)
任选其一。
贡献
除非您明确声明,否则您提交的任何有意包含在作品中的贡献(根据 Apache-2.0 许可证定义),将根据上述方式双授权,不附加任何额外条款或条件。
依赖项
~2.2–8MB
~60K SLoC