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 或 http://www.apache.org/licenses/LICENSE-2.0)
- MIT 许可证 (license/MIT 或 http://opensource.org/licenses/MIT)
由您选择。
贡献
除非您明确声明,否则根据 Apache-2.0 许可证定义的,您提交给作品以包含在内的任何有意贡献,将按照上述方式双许可,不附加任何额外条款或条件。
依赖项
约 340–790KB
~19K SLoC