English version: README.en.md
一个高性能的 Entity Component System (ECS) 库,使用 TypeScript 和 Bun 运行时构建。
- 🚀 高性能:基于 Archetype 的组件存储和高效的查询系统
- 🔧 类型安全:完整的 TypeScript 支持
- 🏗️ 模块化:清晰的架构,支持自定义组件
- 📦 轻量级:零依赖,易于集成
- ⚡ 内存高效:连续内存布局,优化的迭代性能
- 🎣 生命周期钩子:支持多组件和通配符关系的事件监听
bun installimport { World, component } from "@codehz/ecs";
// 定义组件类型
type Position = { x: number; y: number };
type Velocity = { x: number; y: number };
// 定义组件 ID(自动分配)
const PositionId = component<Position>();
const VelocityId = component<Velocity>();
// 创建世界
const world = new World();
// 创建实体并设置组件(所有更改缓冲到 sync() 时应用)
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;
query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
position.x += velocity.x * deltaTime;
position.y += velocity.y * deltaTime;
});component() 自动从全局分配器中分配一个唯一 ID,也可以指定名称或选项:
import { component } from "@codehz/ecs";
// 无参自动分配 ID
const Position = component<Position>();
// 指定名称(序列化时可读)
const Velocity = component<Velocity>("Velocity");
// 带选项的组件(关系专用)
const ChildOf = component({ exclusive: true, name: "ChildOf" });ComponentOptions 选项:
| 选项 | 类型 | 说明 |
|---|---|---|
name |
string |
组件名称,用于序列化/调试 |
exclusive |
boolean |
仅关系组件:同一实体对同一基础组件最多只能有一个关系 |
cascadeDelete |
boolean |
仅实体关系:删除目标实体时,持有该关系的整个实体也会被删除。区别于默认行为(默认仅清理关系组件,实体保留)。支持传递级联。 |
dontFragment |
boolean |
仅关系组件:不同目标实体的关系存放在同一 Archetype,防止因目标不同而过度碎片化 |
merge |
(prev, next) => T |
在同一 sync 批次中对同一组件反复 set() 时的合并策略 |
world.hook() 使用组件数组注册多组件生命周期钩子:
// 返回卸载函数
const unhook = world.hook([PositionId, VelocityId], {
on_init: (entityId, position, velocity) => {
// 钩子注册时,为每个已同时满足条件的实体调用
},
on_set: (entityId, position, velocity) => {
// 当实体「进入」匹配集合时调用(添加/更新组件后)
},
on_remove: (entityId, position, velocity) => {
// 当实体「退出」匹配集合时调用(移除组件或删除实体后)
},
});
// 卸载钩子
unhook();也支持回调简写形式:
const unhook = world.hook([PositionId, VelocityId], (type, entityId, position, velocity) => {
if (type === "init") console.log("初始化");
if (type === "set") console.log("设置");
if (type === "remove") console.log("移除");
});可选组件与过滤器:
// 可选组件:即使 Velocity 不存在也会触发钩子
world.hook([PositionId, { optional: VelocityId }], {
on_set: (entityId, position, velocity) => {
if (velocity !== undefined) {
console.log("拥有速度和位置");
} else {
console.log("仅拥有位置");
}
},
});
// 过滤器:排除带有指定负面组件的实体
const DisabledId = component<void>();
world.hook(
[PositionId, VelocityId],
{
on_set: (entityId, position, velocity) => console.log("进入匹配集合"),
on_remove: (entityId, position, velocity) => console.log("退出匹配集合"),
},
{ negativeComponentTypes: [DisabledId] },
);import { World, component, relation } from "@codehz/ecs";
const ChildOf = component<void>({ exclusive: true });
const world = new World();
const child = world.new();
const parent1 = world.new();
const parent2 = world.new();
// 添加关系
world.set(child, relation(ChildOf, parent1));
world.sync();
// 独占关系:添加新关系时自动移除旧关系
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))); // trueimport { World, component, relation } from "@codehz/ecs";
const PositionId = component<Position>();
const world = new World();
const wildcardPos = relation(PositionId, "*");
// 监听所有该类型关系的变动
world.hook([wildcardPos], {
on_set: (entityId, relations) => {
for (const [targetId, position] of relations) {
console.log(`实体 ${entityId} -> 目标 ${targetId}:`, position);
}
},
on_remove: (entityId, relations) => {
console.log(`实体 ${entityId} 移除了所有 Position 关系`);
},
});const entity = world
.spawn()
.with(Position, { x: 0, y: 0 })
.with(Marker) // void 组件无需传值
.withRelation(ChildOf, parentEntity)
.build();
world.sync(); // 统一应用const entities = world.spawnMany(100, (builder, index) => builder.with(Position, { x: index * 10, y: 0 }));
world.sync();bun run examples/simple.ts
bun run examples/advanced-scheduling.ts
bun run examples/parent-child-hierarchy.ts
bun run examples/inventory-system-relations.ts| 方法 | 说明 |
|---|---|
new<T>() |
创建新实体,返回 EntityId<T> |
create<T>() |
new() 的语义别名 |
spawn() |
返回 EntityBuilder 用于流式创建 |
spawnMany(count, configure) |
批量创建多个实体 |
exists(entity) |
检查实体是否存在 |
set(entity, componentId, data?) |
添加/更新组件(缓冲,sync() 后生效)。对 void 组件可不传 data |
set(componentId, data) |
单例组件简写:world.set(GlobalConfig, { ... }) |
get(entity, componentId?) |
获取组件数据。若组件不存在会抛出异常,请先用 has() 检查或使用 getOptional() |
getOptional(entity, componentId?) |
安全获取组件,返回 { value: T } | undefined |
has(entity, componentId?) |
检查组件是否存在 |
remove(entity, componentId?) |
移除组件(缓冲),也有单例简写 |
delete(entity) |
销毁实体及其所有组件(缓冲) |
query(componentIds) |
快速查询(不缓存) |
query(componentIds, true) |
快速查询并返回实体及组件数据 |
createQuery(componentIds, filter?) |
创建可重用的缓存查询 |
releaseQuery(query) |
释放查询(可选清理) |
hook(componentTypes, hook, filter?) |
注册生命周期钩子,返回卸载函数 |
serialize() |
序列化世界状态为快照对象 |
sync() |
执行所有延迟命令 |
查询通过 world.createQuery() 创建,应跨帧复用以获得最佳性能。
| 方法 | 说明 |
|---|---|
forEach(componentTypes, callback) |
遍历匹配实体 |
getEntities() |
获取所有匹配实体的 ID 列表 |
getEntitiesWithComponents(types) |
获取实体及组件数据的对象数组 |
iterate(types) |
返回生成器,用于 for...of 遍历 |
getComponentData(type) |
获取所有匹配实体的单组件数据数组 |
dispose() |
释放查询(引用计数减一,归零时完全释放) |
get disposed() |
检查查询是否已释放 |
interface QueryFilter {
negativeComponentTypes?: EntityId<any>[]; // 排除的组件
}| 方法 | 说明 |
|---|---|
with(componentId, ...args) |
添加普通组件。void 类型不传值 |
withRelation(componentId, target, ...args) |
添加关系组件。void 类型不传值 |
build() |
创建实体并返回 EntityId(仍需要 sync()) |
// 自动分配 ID
component<T>();
// 指定名称
component<T>("Name");
// 带选项
component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, dontFragment?: boolean, merge?: (prev, next) => T });// 创建关系 ID
relation(componentId, targetEntity);
// 通配符(查询所有目标)
relation(componentId, "*");
// 单例目标(关联到另一个组件)
relation(componentId, otherComponentId);- 组件 ID:
1~1023 - 实体 ID:
1024+ - 关系 ID:负数编码
-(componentId * 2^42 + targetId)
库提供对世界状态的「内存快照」序列化接口,用于保存/恢复实体与组件数据。
// 创建快照(内存对象)
const snapshot = world.serialize();
// 在同一进程内直接恢复
const restored = new World(snapshot);设计要点:
world.serialize()返回内存快照对象,不会对组件值执行JSON.stringify,也不会尝试将组件值转换为可序列化格式。new World(snapshot)是反序列化的唯一入口(没有World.deserialize()静态方法)。- 快照包含实体、组件以及
EntityIdManager分配器状态(保留下一次分配的 ID);不会自动恢复查询缓存或生命周期钩子。
持久化示例(组件值为 JSON 友好时):
const snapshot = world.serialize();
const json = JSON.stringify(snapshot);
// 写入文件或发送到网络 ...
const parsed = JSON.parse(json);
const restored = new World(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 ...
// 恢复时反向解码
const decodedSnapshot = {
...decoded,
entities: decoded.entities.map((e) => ({
id: e.id,
components: e.components.map((c) => ({ type: c.type, value: myDecode(c.value) })),
})),
};
const restored = new World(decodedSnapshot);重要: get() 在组件不存在时会抛出异常。由于 undefined 是组件的有效值,不能用 get() 的返回值是否为 undefined 来判断组件是否存在。请使用 has() 或 getOptional()。
从 v0.4.0 开始,库移除了内置的 System 和 SystemScheduler。推荐使用 @codehz/pipeline 来组织游戏循环,务必在最后一个 pass 调用 world.sync()。
bun add @codehz/pipelineimport { pipeline } from "@codehz/pipeline";
import { World, component } from "@codehz/ecs";
const world = new World();
const movementQuery = world.createQuery([PositionId, VelocityId]);
const gameLoop = pipeline<{ deltaTime: number }>()
.addPass((env) => {
movementQuery.forEach([PositionId, VelocityId], (entity, position, velocity) => {
position.x += velocity.x * env.deltaTime;
position.y += velocity.y * env.deltaTime;
});
})
.addPass(() => {
world.sync(); // 必须作为最后一个 pass
})
.build();
gameLoop({ deltaTime: 0.016 });src/
├── index.ts # 入口文件(统一导出)
├── core/ # 核心实现
│ ├── world.ts # 世界管理
│ ├── archetype.ts # Archetype 系统(高效组件存储)
│ ├── builder.ts # EntityBuilder 流式创建
│ ├── component-registry.ts # 组件注册表
│ ├── component-entity-store.ts # 单例组件存储
│ ├── component-type-utils.ts # 组件类型工具
│ ├── dont-fragment-store.ts # DontFragment 存储
│ ├── entity.ts # 实体/组件/关系类型导出(聚合)
│ ├── entity-types.ts # 实体 ID 类型定义与常量
│ ├── entity-relation.ts # 关系 ID 编码/解码
│ ├── entity-manager.ts # ID 分配器
│ ├── query-registry.ts # 查询注册表
│ ├── serialization.ts # 序列化 ID 编解码
│ ├── world-serialization.ts # 世界序列化/反序列化
│ ├── world-commands.ts # 世界命令
│ ├── world-hooks.ts # 钩子执行逻辑
│ ├── world-references.ts # 实体引用追踪
│ └── types.ts # 类型定义
├── query/ # 查询系统
│ ├── query.ts # Query 类
│ └── filter.ts # 查询过滤器
├── commands/ # 命令缓冲区
├── utils/ # 工具函数
├── testing/ # 测试工具
└── __tests__/ # 单元测试 & 性能测试
examples/
├── advanced-scheduling.ts # Pipeline 调度示例
├── collision-detection.ts # 碰撞检测示例
├── parent-child-hierarchy.ts # 父子层级与 Transform 传播示例
├── serialization.ts # 序列化示例
├── simple.ts # 基本示例
├── spatial-grid.ts # 空间网格示例
├── state-machine.ts # 状态机示例
└── tag-filtering.ts # 标签过滤示例
scripts/
├── build.ts # 构建脚本
└── release.ts # 发布脚本
bun install
bun test # 运行测试
bunx tsc --noEmit # 类型检查
bun run examples/simple.ts # 运行示例
bun run examples/parent-child-hierarchy.ts
bun run scripts/build.ts # 构建MIT
欢迎提交 Issue 和 Pull Request!