Skip to content

codehz/ecs

Repository files navigation

@codehz/ecs

English version: README.en.md

一个高性能的 Entity Component System (ECS) 库,使用 TypeScript 和 Bun 运行时构建。

特性

  • 🚀 高性能:基于 Archetype 的组件存储和高效的查询系统
  • 🔧 类型安全:完整的 TypeScript 支持
  • 🏗️ 模块化:清晰的架构,支持自定义组件
  • 📦 轻量级:零依赖,易于集成
  • ⚡ 内存高效:连续内存布局,优化的迭代性能
  • 🎣 生命周期钩子:支持多组件和通配符关系的事件监听

安装

bun install

用法

基本示例

import { World, component } from "@codehz/ecs";

// 定义组件类型
type Position = { x: number; y: number };
type Velocity = { x: number; y: number };

// 定义组件 ID(自动分配)
const Position = component<Position>();
const Velocity = component<Velocity>();

// 创建世界
const world = new World();

// 创建实体并设置组件(所有更改缓冲到 sync() 时应用)
const entity = world.new();
world.set(entity, Position, { x: 0, y: 0 });
world.set(entity, Velocity, { x: 1, y: 0.5 });
world.sync();

// 创建可重用的查询
const query = world.createQuery([Position, Velocity]);

// 更新循环
const deltaTime = 1.0 / 60.0;
query.forEach([Position, Velocity], (entity, position, velocity) => {
  position.x += velocity.x * deltaTime;
  position.y += velocity.y * deltaTime;
});

定义组件(ID 自动分配)

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 仅实体关系:删除目标实体时,持有该关系的整个实体也会被删除。区别于默认行为(默认仅清理关系组件,实体保留)。支持传递级联。
sparse boolean 仅关系组件:不同目标实体的关系存放在同一 Archetype,防止因目标不同而过度碎片化(旧别名 dontFragment 仍完全兼容)
merge (prev, next) => T 在同一 sync 批次中对同一组件反复 set() 时的合并策略

生命周期钩子

world.hook() 使用组件数组注册多组件生命周期钩子:

// 返回卸载函数
const unhook = world.hook([Position, Velocity], {
  on_init: (entityId, position, velocity) => {
    // 钩子注册时,为每个已同时满足条件的实体调用
  },
  on_set: (entityId, position, velocity) => {
    // 当实体「进入」匹配集合时调用(添加/更新组件后)
  },
  on_remove: (entityId, position, velocity) => {
    // 当实体「退出」匹配集合时调用(移除组件或删除实体后)
  },
});
// 卸载钩子
unhook();

也支持回调简写形式:

const unhook = 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("仅拥有位置");
    }
  },
});

// 过滤器:排除带有指定负面组件的实体
const Disabled = 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";

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))); // true

通配符关系钩子

import { World, component, relation } from "@codehz/ecs";
const Position = component<Position>();

const world = new World();
const wildcardPos = relation(Position, "*");

// 监听所有该类型关系的变动
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 关系`);
  },
});

EntityBuilder 流式创建

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

API 概述

World

方法 说明
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() 序列化世界状态为快照对象
sync() 执行所有延迟命令

单例组件推荐写法:

const config = world.singleton(GlobalConfig);
config.set({ debug: true });
world.sync();

if (config.has()) {
  console.log(config.get());
}

Query

查询通过 world.createQuery() 创建,应跨帧复用以获得最佳性能。

方法 说明
forEach(componentTypes, callback) 遍历匹配实体
getEntities() 获取所有匹配实体的 ID 列表
getEntitiesWithComponents(types) 获取实体及组件数据的对象数组
iterate(types) 返回生成器,用于 for...of 遍历
getComponentData(type) 获取所有匹配实体的单组件数据数组
dispose() 释放查询(引用计数减一,归零时完全释放)
get disposed() 检查查询是否已释放

QueryFilter

interface QueryFilter {
  negativeComponentTypes?: EntityId<any>[]; // 排除的组件
}

EntityBuilder

方法 说明
with(componentId, ...args) 添加普通组件。void 类型不传值
withRelation(componentId, target, ...args) 添加关系组件。void 类型不传值
build() 创建实体并返回 EntityId(仍需要 sync()

component()

// 自动分配 ID
component<T>();
// 指定名称
component<T>("Name");
// 带选项
component<T>({ name?: string, exclusive?: boolean, cascadeDelete?: boolean, sparse?: boolean, dontFragment?: boolean /* 旧别名,完全兼容 */, merge?: (prev, next) => T });

relation()

// 创建关系 ID
relation(componentId, targetEntity);
// 通配符(查询所有目标)
relation(componentId, "*");
// 单例目标(关联到另一个组件)
relation(componentId, otherComponentId);

关系/层级配套工具(新)

为避免用户在父子层级(ChildOf)和库存系统(InInventory)中反复手写 buildChildrenByParent + 递归遍历逻辑,我们提供了配套工具:

const ChildOf = component<void>({ exclusive: true, sparse: true });
const world = new World();
// ... 创建层级 ...

// 推荐直接在 World 实例上使用(API 表面已简化)
const kids = world.getChildren(parent, ChildOf);
const p = world.getParent(child, ChildOf);

for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
  // ...
}

const items = world.getRelationTargets(player, InInventory);
const owners = world.getRelationSources(sword, InInventory);

这些工具全部在 world 实例方法上也有对应(world.getChildren(...) 等),并完整支持数据负载关系、独占/非独占、删除后一致性。

详见 src/relations/hierarchy.ts 和新增的测试。

组件 / 实体 ID 规则

  • 组件 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()

System / Pipeline 集成

从 v0.4.0 开始,库移除了内置的 SystemSystemScheduler。推荐使用 @codehz/pipeline 来组织游戏循环,务必在最后一个 pass 调用 world.sync()

bun add @codehz/pipeline
import { pipeline } from "@codehz/pipeline";
import { World, component } from "@codehz/ecs";

const world = new World();
const movementQuery = world.createQuery([Position, Velocity]);

const gameLoop = 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!

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors