feat(ecs): 核心系统改进 - 句柄、调度、变更检测与查询编译 (#304)

新增功能:
- EntityHandle: 轻量级实体句柄 (28位索引 + 20位代数)
- SystemScheduler: 声明式系统调度,支持 @Stage/@Before/@After/@InSet 装饰器
- EpochManager: 帧级变更检测
- CompiledQuery: 预编译类型安全查询

API 改进:
- EntitySystem 添加 getBefore()/getAfter()/getSets() getter 方法
- Entity 添加 markDirty() 辅助方法
- IScene 添加 epochManager 属性
- CommandBuffer.pendingCount 修正为返回实际操作数

文档更新:
- 更新系统调度和查询相关文档
This commit is contained in:
YHH
2025-12-15 09:17:00 +08:00
committed by GitHub
parent b5158b6ac6
commit cd6ef222d1
27 changed files with 5233 additions and 43 deletions

View File

@@ -430,6 +430,137 @@ class GameManager {
}
```
## 编译查询 (CompiledQuery)
> **v2.4.0+**
CompiledQuery 是一个轻量级的查询工具,提供类型安全的组件访问和变更检测支持。适合临时查询、工具开发和简单的迭代场景。
### 基本用法
```typescript
// 创建编译查询
const query = scene.querySystem.compile(Position, Velocity);
// 类型安全的遍历 - 组件参数自动推断类型
query.forEach((entity, pos, vel) => {
pos.x += vel.vx * deltaTime;
pos.y += vel.vy * deltaTime;
});
// 获取实体数量
console.log(`匹配实体数: ${query.count}`);
// 获取第一个匹配的实体
const first = query.first();
if (first) {
const [entity, pos, vel] = first;
console.log(`第一个实体: ${entity.name}`);
}
```
### 变更检测
CompiledQuery 支持基于 epoch 的变更检测:
```typescript
class RenderSystem extends EntitySystem {
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
private _lastEpoch = 0;
protected onInitialize(): void {
this._query = this.scene!.querySystem.compile(Transform, Sprite);
}
protected process(entities: readonly Entity[]): void {
// 只处理 Transform 或 Sprite 发生变化的实体
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
this.updateRenderData(entity, transform, sprite);
});
// 保存当前 epoch 作为下次检查的起点
this._lastEpoch = this.scene!.epochManager.current;
}
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
// 更新渲染数据
}
}
```
### 函数式 API
CompiledQuery 提供了丰富的函数式 API
```typescript
const query = scene.querySystem.compile(Position, Health);
// map - 转换实体数据
const positions = query.map((entity, pos, health) => ({
x: pos.x,
y: pos.y,
healthPercent: health.current / health.max
}));
// filter - 过滤实体
const lowHealthEntities = query.filter((entity, pos, health) => {
return health.current < health.max * 0.2;
});
// find - 查找第一个匹配的实体
const target = query.find((entity, pos, health) => {
return health.current > 0 && pos.x > 100;
});
// toArray - 转换为数组
const allData = query.toArray();
for (const [entity, pos, health] of allData) {
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
}
// any/empty - 检查是否有匹配
if (query.any()) {
console.log('有匹配的实体');
}
if (query.empty()) {
console.log('没有匹配的实体');
}
```
### CompiledQuery vs EntitySystem
| 特性 | CompiledQuery | EntitySystem |
|------|---------------|--------------|
| **用途** | 轻量级查询工具 | 完整的系统逻辑 |
| **生命周期** | 无 | 完整 (onInitialize, onDestroy 等) |
| **调度集成** | 无 | 支持 @Stage, @Before, @After |
| **变更检测** | forEachChanged | forEachChanged |
| **事件监听** | 无 | addEventListener |
| **命令缓冲** | 无 | this.commands |
| **类型安全组件** | forEach 参数自动推断 | 需要手动 getComponent |
| **适用场景** | 临时查询、工具、原型 | 核心游戏逻辑 |
**选择建议**
- 使用 **EntitySystem** 处理核心游戏逻辑移动、战斗、AI 等)
- 使用 **CompiledQuery** 进行一次性查询、工具开发或简单迭代
### CompiledQuery API 参考
| 方法 | 说明 |
|------|------|
| `forEach(callback)` | 遍历所有匹配实体,类型安全的组件参数 |
| `forEachChanged(sinceEpoch, callback)` | 只遍历变更的实体 |
| `first()` | 获取第一个匹配的实体和组件 |
| `toArray()` | 转换为 [entity, ...components] 数组 |
| `map(callback)` | 映射转换 |
| `filter(predicate)` | 过滤实体 |
| `find(predicate)` | 查找第一个满足条件的实体 |
| `any()` | 是否有任何匹配 |
| `empty()` | 是否没有匹配 |
| `count` | 匹配的实体数量 |
| `entities` | 匹配的实体列表(只读) |
## 最佳实践
### 1. 优先使用 EntitySystem

View File

@@ -672,6 +672,243 @@ scene.addSystem(new SystemB()); // addOrder = 1后执行
> **注意**`addOrder` 由框架在 `addSystem` 时自动设置,无需手动管理。这确保了相同 `updateOrder` 的系统按照添加顺序执行,避免了排序不稳定导致的随机行为。
## 声明式系统调度
> **v2.4.0+**
除了使用 `updateOrder` 手动控制执行顺序外,框架还提供了声明式的系统调度机制,让你可以通过依赖关系来定义系统的执行顺序。
### 调度装饰器
```typescript
import { EntitySystem, ECSSystem, Stage, Before, After, InSet } from '@esengine/ecs-framework';
// 使用装饰器声明系统调度
@ECSSystem('Movement')
@Stage('update') // 在 update 阶段执行
@After('InputSystem') // 在 InputSystem 之后执行
@Before('RenderSystem') // 在 RenderSystem 之前执行
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
// 移动逻辑
}
}
// 使用系统集合进行分组
@ECSSystem('Physics')
@Stage('update')
@InSet('CoreSystems') // 属于 CoreSystems 集合
class PhysicsSystem extends EntitySystem {
// ...
}
@ECSSystem('Collision')
@Stage('update')
@After('set:CoreSystems') // 在 CoreSystems 集合的所有系统之后执行
class CollisionSystem extends EntitySystem {
// ...
}
```
### 系统执行阶段
框架定义了以下系统执行阶段,按顺序执行:
| 阶段 | 说明 | 典型用途 |
|------|------|----------|
| `startup` | 启动阶段 | 一次性初始化 |
| `preUpdate` | 更新前阶段 | 输入处理、状态准备 |
| `update` | 主更新阶段(默认) | 核心游戏逻辑 |
| `postUpdate` | 更新后阶段 | 物理、碰撞检测 |
| `cleanup` | 清理阶段 | 资源清理、状态重置 |
### Fluent API 配置
如果不想使用装饰器,也可以使用 Fluent API 在运行时配置调度:
```typescript
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
// 使用 Fluent API 配置调度
this.stage('update')
.after('InputSystem')
.before('RenderSystem')
.inSet('CoreSystems');
}
}
```
### 循环依赖检测
框架会自动检测循环依赖并抛出明确的错误:
```typescript
// 这会导致循环依赖错误
@ECSSystem('SystemA')
@Before('SystemB')
class SystemA extends EntitySystem { }
@ECSSystem('SystemB')
@Before('SystemA') // 错误A -> B -> A 形成循环
class SystemB extends EntitySystem { }
// 错误信息Cyclic dependency detected: SystemA -> SystemB -> SystemA
```
## 帧级变更检测
> **v2.4.0+**
框架提供了基于 epoch 的帧级变更检测机制,让系统可以只处理发生变化的实体,大幅提升性能。
### 核心概念
- **Epoch**:全局帧计数器,每帧递增
- **lastWriteEpoch**:组件最后被修改时的 epoch
- **变更检测**:通过比较 epoch 判断组件是否在指定时间点后发生变化
### 标记组件为已修改
修改组件数据后,需要标记组件为已变更。有两种方式:
**方式 1通过 Entity 辅助方法(推荐)**
```typescript
// 修改组件后通过 entity.markDirty() 标记
const pos = entity.getComponent(Position)!;
pos.x = 100;
pos.y = 200;
entity.markDirty(pos);
// 可以同时标记多个组件
const vel = entity.getComponent(Velocity)!;
vel.vx = 10;
entity.markDirty(pos, vel);
```
**方式 2在组件内部封装**
```typescript
class VelocityComponent extends Component {
private _vx: number = 0;
private _vy: number = 0;
// 提供修改方法,接收 epoch 参数
public setVelocity(vx: number, vy: number, epoch: number): void {
this._vx = vx;
this._vy = vy;
this.markDirty(epoch);
}
public get vx(): number { return this._vx; }
public get vy(): number { return this._vy; }
}
// 在系统中使用
const vel = entity.getComponent(VelocityComponent)!;
vel.setVelocity(10, 20, this.currentEpoch);
```
### 在系统中使用变更检测
EntitySystem 提供了多个变更检测辅助方法:
```typescript
@ECSSystem('Physics')
class PhysicsSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
// 方式1使用 forEachChanged 只处理变更的实体
// 自动保存 epoch 检查点
this.forEachChanged(entities, [Velocity], (entity) => {
const pos = this.requireComponent(entity, Position);
const vel = this.requireComponent(entity, Velocity);
// 只有 Velocity 变化时才更新位置
pos.x += vel.vx * Time.deltaTime;
pos.y += vel.vy * Time.deltaTime;
});
}
}
@ECSSystem('Transform')
class TransformSystem extends EntitySystem {
constructor() {
super(Matcher.all(Transform, RigidBody));
}
protected process(entities: readonly Entity[]): void {
// 方式2使用 filterChanged 获取变更的实体列表
const changedEntities = this.filterChanged(entities, [RigidBody]);
for (const entity of changedEntities) {
// 处理物理状态变化的实体
this.updatePhysics(entity);
}
// 手动保存 epoch 检查点
this.saveEpoch();
}
protected updatePhysics(entity: Entity): void {
// 物理更新逻辑
}
}
```
### 变更检测 API 参考
| 方法 | 说明 |
|------|------|
| `forEachChanged(entities, [Types], callback)` | 遍历指定组件发生变更的实体,自动保存检查点 |
| `filterChanged(entities, [Types])` | 返回指定组件发生变更的实体数组 |
| `hasChanged(entity, [Types])` | 检查单个实体的指定组件是否发生变更 |
| `saveEpoch()` | 手动保存当前 epoch 作为检查点 |
| `lastProcessEpoch` | 获取上次保存的 epoch 检查点 |
| `currentEpoch` | 获取当前场景的 epoch |
### 使用场景
变更检测特别适合以下场景:
1. **脏标记优化**:只在数据变化时更新渲染
2. **物理同步**:只同步位置/速度发生变化的实体
3. **网络同步**:只发送变化的组件数据
4. **缓存失效**:只在依赖数据变化时重新计算
```typescript
@ECSSystem('NetworkSync')
class NetworkSyncSystem extends EntitySystem {
constructor() {
super(Matcher.all(NetworkComponent, Transform));
}
protected process(entities: readonly Entity[]): void {
// 只同步变化的实体,大幅减少网络流量
this.forEachChanged(entities, [Transform], (entity) => {
const transform = this.requireComponent(entity, Transform);
const network = this.requireComponent(entity, NetworkComponent);
this.sendTransformUpdate(network.id, transform);
});
}
private sendTransformUpdate(id: string, transform: Transform): void {
// 发送网络更新
}
}
```
## 复杂系统示例
### 碰撞检测系统