An open API service indexing awesome lists of open source software.

https://github.com/codehz/ecs


https://github.com/codehz/ecs

Last synced: 8 days ago
JSON representation

Awesome Lists containing this project

README

          

# @codehz/ecs

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

## 特性

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

## 安装

```bash
bun install
```

## 用法

### 基本示例

```typescript
import { 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(1);
const VelocityId = component(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 支持在组件添加或移除时执行回调函数。钩子回调函数的参数如下:

- `entityId`: 实体的 ID (number)
- `componentType`: 组件类型 ID (EntityId)
- `component`: 组件数据值 (T)

```typescript
// 注册组件生命周期钩子
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 还支持多组件生命周期钩子,可以监听多个组件同时存在于实体时的事件。只有当所有必需组件都存在时才会触发回调。

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

// 定义组件ID
const PositionId = component();
const VelocityId = component();

// 注册多组件生命周期钩子
world.hook([PositionId, VelocityId], {
on_init: (entityId, componentTypes, components) => {
// 当钩子注册时,为已同时拥有 Position 和 Velocity 组件的实体调用
console.log(`实体 ${entityId} 同时拥有 Position 和 Velocity 组件`);
},
on_set: (entityId, componentTypes, components) => {
// 当实体同时拥有 Position 和 Velocity 组件时调用
const [position, velocity] = components;
console.log(
`实体 ${entityId} 现在同时拥有 Position (${position.x}, ${position.y}) 和 Velocity (${velocity.x}, ${velocity.y})`,
);
},
on_remove: (entityId, componentTypes, components) => {
// 当实体失去 Position 或 Velocity 组件之一时调用(如果之前同时拥有两者)
const [position, velocity] = components; // 移除前的组件值快照
console.log(`实体 ${entityId} 失去了 Position 或 Velocity 组件`);
},
});

// 添加组件
const entity = world.new();
world.set(entity, PositionId, { x: 0, y: 0 });
world.set(entity, VelocityId, { x: 1, y: 0.5 });
world.sync(); // 多组件钩子在这里被调用
```

还可以使用可选组件,这样即使某些组件不存在也会触发钩子:

```typescript
// 注册包含可选组件的多组件生命周期钩子
world.hook([PositionId, { optional: VelocityId }], {
on_set: (entityId, componentTypes, components) => {
// 当实体拥有 Position 组件时调用,Velocity 组件可选
const [position, velocity] = components;
if (velocity !== undefined) {
console.log(`实体 ${entityId} 拥有 Position 和 Velocity 组件`);
} else {
console.log(`实体 ${entityId} 仅拥有 Position 组件`);
}
},
});
```

### 通配符关系生命周期钩子

ECS 还支持通配符关系生命周期钩子,可以监听特定组件的所有关系变化:

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

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

// 定义组件ID
const PositionId = component(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(); // 通配符钩子会被触发
```

### Exclusive Relations

ECS 支持 Exclusive Relations,确保实体对于指定的组件类型最多只能有一个关系。当添加新的关系时,会自动移除之前的所有同类型关系:

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

// 定义组件ID,设置为独占关系
const ChildOf = component({ 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();
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))); // true
```

### 运行示例

```bash
bun run demo
```

或者直接运行:

```bash
bun run examples/simple/demo.ts
```

## API 概述

### World

- `new()`: 创建新实体
- `spawn()`: 创建 EntityBuilder 用于流式实体创建
- `spawnMany(count, configure)`: 批量创建多个实体
- `exists(entity)`: 检查实体是否存在
- `set(entity, componentId, data)`: 向实体添加组件
- `get(entity, componentId)`: 获取实体的组件数据(注意:只能获取已设置的组件,使用前请先用 `has()` 检查组件是否存在)
- `has(entity, componentId)`: 检查实体是否拥有指定组件
- `remove(entity, componentId)`: 从实体移除组件
- `delete(entity)`: 销毁实体及其所有组件
- `query(componentIds)`: 快速查询具有指定组件的实体
- `createQuery(componentIds)`: 创建可重用的查询对象
- `hook(componentId, hook)`: 注册组件或通配符关系生命周期钩子
- `unhook(componentId, hook)`: 注销组件或通配符关系生命周期钩子
- `serialize()`: 序列化世界状态为快照对象
- `sync()`: 执行所有延迟命令

### 序列化(快照)

库提供了对世界状态的「内存快照」序列化接口,用于保存/恢复实体与组件的数据。注意关键点:

- `world.serialize()` 返回一个内存中的快照对象(snapshot),快照会按引用保存组件的实际值;它不会对数据做 JSON.stringify 操作,也不会尝试把组件值转换为可序列化格式。
- `new World(snapshot)` 通过构造函数接受由 `world.serialize()` 生成的快照对象并重建世界状态。它期望一个内存对象(非 JSON 字符串)。

为什么采用这种设计?很多情况下组件值可能包含函数、类实例、循环引用或其他无法用 JSON 表示的值。库不对组件值强行进行序列化/字符串化,以避免数据丢失或不可信的自动转换。

示例:内存回环(component 值可为任意对象)

```ts
// 获取快照(内存对象)
const snapshot = world.serialize();

// 在同一进程内直接恢复
const restored = new World(snapshot);
```

持久化到磁盘或跨进程传输

如果你需要把世界保存到文件或通过网络传输,需要自己实现组件值的编码/解码策略:

1. 使用 `World.serialize()` 得到 snapshot。
2. 对 snapshot 中的组件值逐项进行可自定义的编码(例如将类实例转成纯数据、把函数替换为标识符,或使用自定义二进制编码)。
3. 将编码后的对象字符串化并持久化。恢复时执行相反的解码步骤,得到与 `World.serialize()` 兼容的快照对象,然后调用 `World.deserialize(decodedSnapshot)`。

简单示例:当组件值都是 JSON-友好时

```ts
const snapshot = world.serialize();
// 如果组件值都可 JSON 化,可以直接 stringify
const text = JSON.stringify(snapshot);
// 写入文件或发送到网络

// 恢复:parse -> deserialize
const parsed = JSON.parse(text);
const restored = new World(parsed);
```

示例:带自定义编码的持久化(伪代码)

```ts
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 = new World(readySnapshot);
```

注意事项

- **重要警告**:`get()` 方法只能获取实体已设置的组件。如果尝试获取不存在的组件,会抛出错误。由于 `undefined` 是组件的有效值,不能使用 `get()` 的返回值是否为 `undefined` 来判断组件是否存在。请在使用 `get()` 之前先用 `has()` 方法检查组件是否存在。
- 快照只包含实体、组件、以及 `EntityIdManager` 的分配器状态(用于保留下一次分配的 ID);并不会自动恢复查询缓存或生命周期钩子。恢复后应由应用负责重新注册钩子。
- 若需要跨版本兼容,建议在持久化格式中包含 `version` 字段,并在恢复时进行格式兼容性检查与迁移。

### Entity

- `component(id)`: 分配类型安全的组件ID(上限:1022个)

### Query

- `forEach(componentIds, callback)`: 遍历匹配的实体,为每个实体调用回调函数
- `getEntities()`: 获取所有匹配实体的ID列表
- `getEntitiesWithComponents(componentIds)`: 获取实体及其组件数据的对象数组
- `iterate(componentIds)`: 返回一个生成器,用于遍历匹配的实体及其组件数据
- `getComponentData(componentType)`: 获取指定组件类型的所有匹配实体的数据数组
- `dispose()`: 释放查询资源,停止接收世界更新通知

### EntityBuilder

EntityBuilder 提供流式 API 用于便捷的实体创建:

- `with(componentId, value)`: 添加组件到构建器
- `withTag(componentId)`: 添加标记组件(无值)到构建器
- `withRelation(componentId, targetEntity, value)`: 添加关系组件到构建器
- `withRelationTag(componentId, targetEntity)`: 添加关系标记(无值)到构建器
- `build()`: 创建实体并应用所有组件(需要手动调用 `world.sync()`)

### World

从 v0.4.0 开始,本库移除了内置的 `System` 和 `SystemScheduler` 功能。推荐使用 `@codehz/pipeline` 作为替代方案来组织游戏循环逻辑。

### 为什么移除 System?

- **简化库的维护**:System 调度器增加了代码复杂度,但其功能可以通过更通用的 pipeline 模式实现
- **更灵活的执行控制**:Pipeline 模式允许更细粒度的控制,支持异步操作和条件执行
- **更好的关注点分离**:ECS 库专注于实体和组件管理,系统调度由外部库处理

### 迁移示例

**旧代码(使用 System)**:

```typescript
import { World, component } from "@codehz/ecs";
import type { System } from "@codehz/ecs";

class MovementSystem implements System<[deltaTime: number]> {
private query: Query;

constructor(world: World<[deltaTime: number]>) {
this.query = world.createQuery([PositionId, VelocityId]);
}

update(deltaTime: number): void {
this.query.forEach([PositionId, VelocityId], (entity, position, velocity) => {
position.x += velocity.x * deltaTime;
position.y += velocity.y * deltaTime;
});
}
}

const world = new World<[deltaTime: number]>();
world.registerSystem(new MovementSystem(world));
world.update(0.016); // 自动调用 sync()
```

**新代码(使用 Pipeline)**:

```typescript
import { 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;
});
})
// 重要:world.sync() 必须作为最后一个 pass 调用,以还原之前 world.update() 的自动提交行为
.addPass(() => {
world.sync();
})
.build();

gameLoop({ deltaTime: 0.016 });
```

### 关键变化

1. **移除泛型参数**:`World` 不再需要 `UpdateParams` 泛型参数
2. **移除的方法**:`registerSystem()` 和 `update()` 方法已移除
3. **手动调用 sync()**:之前 `world.update()` 会自动调用 `sync()`,现在需要在 pipeline 末尾显式调用
4. **执行顺序**:Pass 的执行顺序由添加顺序决定,无需手动声明依赖关系

### 安装 Pipeline

```bash
bun add @codehz/pipeline
```

## 性能特点

- **Archetype 系统**:实体按组件组合分组,实现连续内存访问
- **缓存查询**:查询结果自动缓存,减少重复计算
- **命令缓冲区**:延迟执行组件添加/移除,提高批处理效率
- **类型安全**:编译时类型检查,无运行时开销

## 开发

### 运行测试

```bash
bun test
```

### 类型检查

```bash
bunx tsc --noEmit
```

## 项目结构

```
src/
├── index.ts # 入口文件
├── entity.ts # 实体和组件管理
├── world.ts # 世界管理
├── archetype.ts # Archetype 系统(高效组件存储)
├── query.ts # 查询系统
├── query-filter.ts # 查询过滤器
├── command-buffer.ts # 命令缓冲区
├── types.ts # 类型定义
├── utils.ts # 工具函数
├── *.test.ts # 单元测试
├── query.example.ts # 查询示例
└── *.perf.test.ts # 性能测试

examples/
├── simple/
│ ├── demo.ts # 基本示例
│ └── README.md # 示例说明
└── advanced-scheduling/
└── demo.ts # Pipeline 调度示例

scripts/
├── build.ts # 构建脚本
└── release.ts # 发布脚本
```

## 许可证

MIT

## 贡献

欢迎提交 Issue 和 Pull Request!