English version:README.en.md
一个高性能的 Entity Component System (ECS) 库,使用 TypeScript 和 Bun 运行时构建。
- 🚀 高性能:基于 Archetype 的组件存储和高效的查询系统
- 🔧 类型安全:完整的 TypeScript 支持
- 🏗️ 模块化:清晰的架构,支持自定义组件
- 📦 轻量级:零依赖,易于集成
- ⚡ 内存高效:连续内存布局,优化的迭代性能
- 🎣 生命周期钩子:支持多组件和通配符关系的事件监听
bun installimport{World,component}from"@codehz/ecs";// 定义组件类型typePosition={x: number;y: number};typeVelocity={x: number;y: number};// 定义组件 ID(自动分配)constPosition=component<Position>();constVelocity=component<Velocity>();// 创建世界constworld=newWorld();// 创建实体并设置组件(所有更改缓冲到 sync() 时应用)constentity=world.new();world.set(entity,Position,{x: 0,y: 0});world.set(entity,Velocity,{x: 1,y: 0.5});world.sync();// 创建可重用的查询constquery=world.createQuery([Position,Velocity]);// 更新循环constdeltaTime=1.0/60.0;query.forEach([Position,Velocity],(entity,position,velocity)=>{position.x+=velocity.x*deltaTime;position.y+=velocity.y*deltaTime;});component() 自动从全局分配器中分配一个唯一 ID,也可以指定名称或选项:
import{component}from"@codehz/ecs";// 无参自动分配 IDconstPosition=component<Position>();// 指定名称(序列化时可读)constVelocity=component<Velocity>("Velocity");// 带选项的组件(关系专用)constChildOf=component({exclusive: true,name: "ChildOf"});ComponentOptions 选项:
| 选项 | 类型 | 说明 |
|---|---|---|
name | string | 组件名称,用于序列化/调试 |
exclusive | boolean | 仅关系组件:同一实体对同一基础组件最多只能有一个关系 |
cascadeDelete | boolean | 仅实体关系:删除目标实体时,持有该关系的整个实体也会被删除。区别于默认行为(默认仅清理关系组件,实体保留)。支持传递级联。 |
sparse | boolean | 仅关系组件:不同目标实体的关系存放在同一 Archetype,防止因目标不同而过度碎片化(旧别名 dontFragment已弃用,下一 major 删除) |
merge | (prev, next) => T | 在同一 sync 批次中对同一组件反复 set() 时的合并策略 |
world.hook() 使用组件数组注册多组件生命周期钩子:
// 返回卸载函数constunhook=world.hook([Position,Velocity],{on_init: (entityId,position,velocity)=>{// 钩子注册时,为每个已同时满足条件的实体调用},on_set: (entityId,position,velocity)=>{// 当实体「进入」匹配集合时调用(添加/更新组件后)},on_remove: (entityId,position,velocity)=>{// 当实体「退出」匹配集合时调用(移除组件或删除实体后)},});// 卸载钩子unhook();也支持回调简写形式:
constunhook=world.hook([Position,Velocity],(type,entityId,position,velocity)=>{if(type==="init")console.log("初始化");if(type==="set")console.log("设置");if(type==="remove")console.log("移除");});可选组件与过滤器:
// 可选组件:即使 Velocity 不存在也会触发钩子world.hook([Position,{optional: Velocity}],{on_set: (entityId,position,velocity)=>{if(velocity!==undefined){console.log("拥有速度和位置");}else{console.log("仅拥有位置");}},});// 过滤器:排除带有指定负面组件的实体constDisabled=component<void>();world.hook([Position,Velocity],{on_set: (entityId,position,velocity)=>console.log("进入匹配集合"),on_remove: (entityId,position,velocity)=>console.log("退出匹配集合"),},{negativeComponentTypes: [Disabled]},);import{World,component,relation}from"@codehz/ecs";constChildOf=component<void>({exclusive: true});constworld=newWorld();constchild=world.new();constparent1=world.new();constparent2=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)));// falseconsole.log(world.has(child,relation(ChildOf,parent2)));// trueimport{World,component,relation}from"@codehz/ecs";constPosition=component<Position>();constworld=newWorld();constwildcardPos=relation(Position,"*");// 监听所有该类型关系的变动world.hook([wildcardPos],{on_set: (entityId,relations)=>{for(const[targetId,position]ofrelations){console.log(`实体 ${entityId} -> 目标 ${targetId}:`,position);}},on_remove: (entityId,relations)=>{console.log(`实体 ${entityId} 移除了所有 Position 关系`);},});constentity=world.spawn().with(Position,{x: 0,y: 0}).with(Marker)// void 组件无需传值.withRelation(ChildOf,parentEntity).build();world.sync();// 统一应用constentities=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 |
singleton(componentId) | 获取单例组件句柄,推荐用 world.singleton(Config).set(value) |
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() | 序列化世界状态为快照对象(省略 skipSerialize 组件) |
dump() | 调试导出完整世界状态(包含 skipSerialize 组件;不可用于恢复) |
sync() | 执行所有延迟命令 |
单例组件推荐写法:
constconfig=world.singleton(GlobalConfig);config.set({debug: true});world.sync();if(config.has()){console.log(config.get());}查询通过 world.createQuery() 创建,应跨帧复用以获得最佳性能。
| 方法 | 说明 |
|---|---|
forEach(componentTypes, callback) | 遍历匹配实体 |
getEntities() | 获取所有匹配实体的 ID 列表 |
getEntitiesWithComponents(types) | 获取实体及组件数据的对象数组 |
iterate(types) | 返回生成器,用于 for...of 遍历 |
getComponentData(type) | 获取所有匹配实体的单组件数据数组 |
dispose() | 释放查询(引用计数减一,归零时完全释放) |
get disposed() | 检查查询是否已释放 |
interfaceQueryFilter{negativeComponentTypes?: EntityId<any>[];// 排除的组件}| 方法 | 说明 |
|---|---|
with(componentId, ...args) | 添加普通组件。void 类型不传值 |
withRelation(componentId, target, ...args) | 添加关系组件。void 类型不传值 |
build() | 创建实体并返回 EntityId(仍需要 sync()) |
// 自动分配 IDcomponent<T>();// 指定名称component<T>("Name");// 带选项component<T>({name?: string,exclusive?: boolean,cascadeDelete?: boolean,sparse?: boolean,dontFragment?: boolean/* @deprecated,下一 major 删除 */,merge?: (prev,next)=>T});以下符号/行为在当前 major 仍可用,但已弃用,将在 下一 major 移除:
| 项目 | 替代 |
|---|---|
component({ dontFragment: true }) | component({ sparse: true }) |
isDontFragmentComponent / isDontFragmentRelation / isDontFragmentWildcard | isSparseComponent / isSparseRelation / isSparseWildcard |
world.set(componentId, value) singleton 简写 | world.singleton(componentId).set(value) 或 world.set(componentId, componentId, value) |
// 创建关系 IDrelation(componentId,targetEntity);// 通配符(查询所有目标)relation(componentId,"*");// 单例目标(关联到另一个组件)relation(componentId,otherComponentId);为避免用户在父子层级(ChildOf)和库存系统(InInventory)中反复手写 buildChildrenByParent + 递归遍历逻辑,我们提供了配套工具:
constChildOf=component<void>({exclusive: true,sparse: true});constworld=newWorld();// ... 创建层级 ...// 推荐直接在 World 实例上使用(API 表面已简化)constkids=world.getChildren(parent,ChildOf);constp=world.getParent(child,ChildOf);for(const{ entity, depth, parent }ofworld.iterateDescendants(root,ChildOf)){// ...}constitems=world.getRelationTargets(player,InInventory);constowners=world.getRelationSources(sword,InInventory);这些工具全部在 world 实例方法上也有对应(world.getChildren(...) 等),并完整支持数据负载关系、独占/非独占、删除后一致性。
详见 src/relations/hierarchy.ts 和新增的测试。
- 组件 ID:
1~1023 - 实体 ID:
1024+ - 关系 ID:负数编码
-(componentId * 2^42 + targetId)
库提供对世界状态的「内存快照」序列化接口,用于保存/恢复实体与组件数据;另有调试用的完整导出。
// 创建快照(内存对象)—— 省略 skipSerialize 组件constsnapshot=world.serialize();// 在同一进程内直接恢复constrestored=newWorld(snapshot);// 调试导出(形状相同,但包含 skipSerialize 组件;不要用于 new World(...))constdebug=world.dump();| API | 用途 | skipSerialize 组件 |
|---|---|---|
world.serialize() | 存档 / 网络快照 | 省略 |
world.dump() | 调试 / 日志 / 观测 | 包含 |
new World(snapshot) | 从 serialize() 快照恢复 | 脏数据中的 skip 条目仍会丢弃 |
设计要点:
world.serialize()/world.dump()均返回内存对象,不会对组件值执行JSON.stringify,也不会 deep clone;组件值为浅引用。new World(snapshot)是反序列化的唯一入口(没有World.deserialize()静态方法)。不要用dump()的结果恢复世界。- 快照包含实体、组件以及
EntityIdManager分配器状态(保留下一次分配的 ID);不会自动恢复查询缓存或生命周期钩子。
持久化示例(组件值为 JSON 友好时):
constsnapshot=world.serialize();constjson=JSON.stringify(snapshot);// 写入文件或发送到网络 ...constparsed=JSON.parse(json);constrestored=newWorld(parsed);自定义编码示例:
constsnapshot=world.serialize();constencoded={
...snapshot,entities: snapshot.entities.map((e)=>({id: e.id,components: e.components.map((c)=>({type: c.type,value: myEncode(c.value)})),})),};// 持久化 encoded ...// 恢复时反向解码constdecodedSnapshot={
...decoded,entities: decoded.entities.map((e)=>({id: e.id,components: e.components.map((c)=>({type: c.type,value: myDecode(c.value)})),})),};constrestored=newWorld(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";constworld=newWorld();constmovementQuery=world.createQuery([Position,Velocity]);constgameLoop=pipeline<{deltaTime: number}>().addPass((env)=>{movementQuery.forEach([Position,Velocity],(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 # 组件类型工具
│ ├── store.ts # SparseStore (内部稀疏存储)
│ ├── 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!