一个高性能的Entity Component System (ECS) 库,使用 TypeScript 和 Bun 运行时构建。
- 🚀 高性能:基于 Archetype 的组件存储和高效的查询系统
- 🔧 类型安全:完整的 TypeScript 支持
- 🏗️ 模块化:清晰的架构,支持自定义系统和组件
- 📦 轻量级:零依赖,易于集成
- ⚡ 内存高效:连续内存布局,优化的迭代性能
- 🎣 生命周期钩子:支持组件和通配符关系的事件监听
- 🔄 系统调度:支持系统依赖关系和拓扑排序执行
bun installimport { World } from "@codehz/ecs";
import { component } from "@codehz/ecs";
// 定义组件类型
type Position = { x: number; y: number };
type Velocity = { x: number; y: number };
// 定义组件ID
const PositionId = component<Position>(1);
const VelocityId = component<Velocity>(2);
// 创建世界
const world = new World();
// 创建实体
const entity = world.new();
world.set(entity, PositionId, { x: 0, y: 0 });
world.set(entity, VelocityId, { x: 1, y: 0.5 });
// 应用更改
world.sync();
// 创建查询并更新
const query = world.createQuery([PositionId, VelocityId]);
const deltaTime = 1.0 / 60.0; // 假设60FPS
query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
position.x += velocity.x * deltaTime;
position.y += velocity.y * deltaTime;
});ECS 支持在组件添加或移除时执行回调函数:
// 注册组件生命周期钩子
world.hook(PositionId, {
on_init: (entityId, componentType, component) => {
// 当钩子注册时,为现有实体上的组件调用
console.log(`现有组件 ${componentType} 在实体 ${entityId}`);
},
on_set: (entityId, componentType, component) => {
console.log(`组件 ${componentType} 被添加到实体 ${entityId}`);
},
on_remove: (entityId, componentType, component) => {
console.log(`组件 ${componentType} 被从实体 ${entityId} 移除`);
},
});
// 你也可以只注册其中一个钩子
world.hook(VelocityId, {
on_remove: (entityId, componentType, component) => {
console.log(`组件 ${componentType} 被从实体 ${entityId} 移除`);
},
});
// 添加组件时会触发钩子
world.set(entity, PositionId, { x: 0, y: 0 });
world.sync(); // 钩子在这里被调用ECS 还支持通配符关系生命周期钩子,可以监听特定组件的所有关系变化:
import { World, component, relation } from "@codehz/ecs";
// 定义组件类型
type Position = { x: number; y: number };
// 定义组件ID
const PositionId = component<Position>(1);
// 创建世界
const world = new World();
// 创建实体
const entity = world.new();
// 创建通配符关系ID,用于监听所有 Position 相关的关系
const wildcardPositionRelation = relation(PositionId, "*");
// 注册通配符关系钩子
world.hook(wildcardPositionRelation, {
on_set: (entityId, componentType, component) => {
console.log(`关系组件 ${componentType} 被添加到实体 ${entityId}`);
},
on_remove: (entityId, componentType, component) => {
console.log(`关系组件 ${componentType} 被从实体 ${entityId} 移除`);
},
});
// 创建实体间的关系
const entity2 = world.new();
const positionRelation = relation(PositionId, entity2);
world.set(entity, positionRelation, { x: 10, y: 20 });
world.sync(); // 通配符钩子会被触发ECS 支持 Exclusive Relations,确保实体对于指定的组件类型最多只能有一个关系。当添加新的关系时,会自动移除之前的所有同类型关系:
import { World, component, relation } from "@codehz/ecs";
// 定义组件ID
const ChildOf = component(); // 空组件,用于关系
// 创建世界
const world = new World();
// 设置 ChildOf 为独占关系
world.setExclusive(ChildOf);
// 创建实体
const child = world.new();
const parent1 = world.new();
const parent2 = world.new();
// 添加第一个关系
world.set(child, relation(ChildOf, parent1));
world.sync();
console.log(world.has(child, relation(ChildOf, parent1))); // true
// 添加第二个关系 - 会自动移除第一个
world.set(child, relation(ChildOf, parent2));
world.sync();
console.log(world.has(child, relation(ChildOf, parent1))); // false
console.log(world.has(child, relation(ChildOf, parent2))); // truebun run demo或者直接运行:
bun run examples/simple/demo.tsnew(): 创建新实体set(entity, componentId, data): 向实体添加组件get(entity, componentId): 获取实体的组件数据(注意:只能获取已设置的组件,使用前请先用has()检查组件是否存在)has(entity, componentId): 检查实体是否拥有指定组件delete(entity, componentId): 从实体移除组件setExclusive(componentId): 将组件标记为独占关系createQuery(componentIds): 创建查询registerSystem(system, dependencies?): 注册系统hook(componentId, hook): 注册组件或通配符关系生命周期钩子unhook(componentId, hook): 注销组件或通配符关系生命周期钩子update(...params): 更新世界(参数取决于泛型配置)sync(): 应用命令缓冲区
库提供了对世界状态的「内存快照」序列化接口,用于保存/恢复实体与组件的数据。注意关键点:
World.serialize()返回一个内存中的快照对象(snapshot),快照会按引用保存组件的实际值;它不会对数据做 JSON.stringify 操作,也不会尝试把组件值转换为可序列化格式。World.deserialize(snapshot)接受由World.serialize()生成的快照对象并重建世界状态。它期望一个内存对象(非 JSON 字符串)。
为什么采用这种设计?很多情况下组件值可能包含函数、类实例、循环引用或其他无法用 JSON 表示的值。库不对组件值强行进行序列化/字符串化,以避免数据丢失或不可信的自动转换。
示例:内存回环(component 值可为任意对象)
// 获取快照(内存对象)
const snapshot = world.serialize();
// 在同一进程内直接恢复
const restored = World.deserialize(snapshot);持久化到磁盘或跨进程传输
如果你需要把世界保存到文件或通过网络传输,需要自己实现组件值的编码/解码策略:
- 使用
World.serialize()得到 snapshot。 - 对 snapshot 中的组件值逐项进行可自定义的编码(例如将类实例转成纯数据、把函数替换为标识符,或使用自定义二进制编码)。
- 将编码后的对象字符串化并持久化。恢复时执行相反的解码步骤,得到与
World.serialize()兼容的快照对象,然后调用World.deserialize(decodedSnapshot)。
简单示例:当组件值都是 JSON-友好时
const snapshot = world.serialize();
// 如果组件值都可 JSON 化,可以直接 stringify
const text = JSON.stringify(snapshot);
// 写入文件或发送到网络
// 恢复:parse -> deserialize
const parsed = JSON.parse(text);
const restored = World.deserialize(parsed);示例:带自定义编码的持久化(伪代码)
const snapshot = world.serialize();
// 将组件值编码为可持久化格式
const encoded = {
...snapshot,
entities: snapshot.entities.map((e) => ({
id: e.id,
components: e.components.map((c) => ({ type: c.type, value: myEncode(c.value) })),
})),
};
// 持久化 encoded(JSON.stringify / 二进制写入等)
// 恢复时解码回原始组件值
const decoded = /* parse file and decode */ encoded;
const readySnapshot = {
...decoded,
entities: decoded.entities.map((e) => ({
id: e.id,
components: e.components.map((c) => ({ type: c.type, value: myDecode(c.value) })),
})),
};
const restored = World.deserialize(readySnapshot);注意事项
- 重要警告:
get()方法只能获取实体已设置的组件。如果尝试获取不存在的组件,会抛出错误。由于undefined是组件的有效值,不能使用get()的返回值是否为undefined来判断组件是否存在。请在使用get()之前先用has()方法检查组件是否存在。 - 快照只包含实体、组件、以及
EntityIdManager的分配器状态(用于保留下一次分配的 ID);并不会自动恢复已注册的系统、查询缓存或生命周期钩子。恢复后应由应用负责重新注册系统与钩子。 - 若需要跨版本兼容,建议在持久化格式中包含
version字段,并在恢复时进行格式兼容性检查与迁移。
component<T>(id): 分配类型安全的组件ID(上限:1022个)
forEach(componentIds, callback): 遍历匹配的实体getEntities(): 获取所有匹配实体的ID列表getEntitiesWithComponents(componentIds): 获取实体及其组件数据
实现 System 接口来创建自定义系统:
class MySystem implements System {
update(): void {
// 系统逻辑
}
}如果需要接收额外参数(如时间增量),可以指定泛型参数:
class MovementSystem implements System<[deltaTime: number]> {
update(deltaTime: number): void {
// 使用 deltaTime 更新位置
}
}系统支持依赖关系排序,确保正确的执行顺序。依赖关系可以通过系统的 dependencies 属性指定:
class InputSystem implements System<[deltaTime: number]> {
readonly dependencies: readonly System<[deltaTime: number]>[] = [];
update(deltaTime: number): void {
// 处理输入
}
}
class MovementSystem implements System<[deltaTime: number]> {
readonly dependencies: readonly System<[deltaTime: number]>[];
constructor(inputSystem: InputSystem) {
this.dependencies = [inputSystem]; // 指定依赖
}
update(deltaTime: number): void {
// 更新位置
}
}
// 注册系统
const inputSystem = new InputSystem();
world.registerSystem(inputSystem);
world.registerSystem(new MovementSystem(inputSystem), [inputSystem]); // 也可以在注册时指定额外依赖系统将按照拓扑排序执行,依赖系统始终在被依赖系统之前运行。
- Archetype 系统:实体按组件组合分组,实现连续内存访问
- 缓存查询:查询结果自动缓存,减少重复计算
- 命令缓冲区:延迟执行组件添加/移除,提高批处理效率
- 类型安全:编译时类型检查,无运行时开销
bun testbunx tsc --noEmitsrc/
├── index.ts # 入口文件
├── entity.ts # 实体和组件管理
├── world.ts # 世界管理
├── archetype.ts # Archetype 系统(高效组件存储)
├── query.ts # 查询系统
├── query-filter.ts # 查询过滤器
├── system.ts # 系统接口
├── system-scheduler.ts # 系统调度器
├── command-buffer.ts # 命令缓冲区
├── types.ts # 类型定义
├── utils.ts # 工具函数
├── *.test.ts # 单元测试
├── query.example.ts # 查询示例
└── *.perf.test.ts # 性能测试
examples/
└── simple/
├── demo.ts # 基本示例
└── README.md # 示例说明
scripts/
├── build.ts # 构建脚本
└── release.ts # 发布脚本
MIT
欢迎提交 Issue 和 Pull Request!