From cd6ef222d16261d6fc22a9fe3388c6b1c91ca330 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Mon, 15 Dec 2025 09:17:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(ecs):=20=E6=A0=B8=E5=BF=83=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E6=94=B9=E8=BF=9B=20-=20=E5=8F=A5=E6=9F=84=E3=80=81?= =?UTF-8?q?=E8=B0=83=E5=BA=A6=E3=80=81=E5=8F=98=E6=9B=B4=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E4=B8=8E=E6=9F=A5=E8=AF=A2=E7=BC=96=E8=AF=91=20(#304)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - EntityHandle: 轻量级实体句柄 (28位索引 + 20位代数) - SystemScheduler: 声明式系统调度,支持 @Stage/@Before/@After/@InSet 装饰器 - EpochManager: 帧级变更检测 - CompiledQuery: 预编译类型安全查询 API 改进: - EntitySystem 添加 getBefore()/getAfter()/getSets() getter 方法 - Entity 添加 markDirty() 辅助方法 - IScene 添加 epochManager 属性 - CommandBuffer.pendingCount 修正为返回实际操作数 文档更新: - 更新系统调度和查询相关文档 --- docs/en/guide/system.md | 237 ++++++++ docs/guide/entity-query.md | 131 +++++ docs/guide/system.md | 237 ++++++++ packages/core/src/ECS/Component.ts | 44 ++ packages/core/src/ECS/Core/CommandBuffer.ts | 356 +++++++++-- packages/core/src/ECS/Core/EntityHandle.ts | 136 +++++ .../core/src/ECS/Core/EntityHandleManager.ts | 331 +++++++++++ packages/core/src/ECS/Core/EpochManager.ts | 103 ++++ .../core/src/ECS/Core/Query/CompiledQuery.ts | 366 ++++++++++++ packages/core/src/ECS/Core/QuerySystem.ts | 108 ++++ .../src/ECS/Core/SystemDependencyGraph.ts | 263 +++++++++ packages/core/src/ECS/Core/SystemScheduler.ts | 338 +++++++++++ .../src/ECS/Decorators/SystemScheduling.ts | 177 ++++++ packages/core/src/ECS/Decorators/index.ts | 14 + packages/core/src/ECS/Entity.ts | 75 +++ packages/core/src/ECS/IScene.ts | 11 + packages/core/src/ECS/Scene.ts | 126 +++- packages/core/src/ECS/Systems/EntitySystem.ts | 321 ++++++++++ packages/core/src/ECS/index.ts | 32 + packages/core/src/Types/TypeHelpers.ts | 7 +- packages/core/src/Types/index.ts | 16 + .../core/tests/ECS/Core/CommandBuffer.test.ts | 4 +- .../core/tests/ECS/Core/CompiledQuery.test.ts | 373 ++++++++++++ .../core/tests/ECS/Core/EntityHandle.test.ts | 380 ++++++++++++ .../core/tests/ECS/Core/EpochManager.test.ts | 72 +++ .../tests/ECS/Core/SystemScheduler.test.ts | 555 ++++++++++++++++++ .../EntitySystemChangeDetection.test.ts | 463 +++++++++++++++ 27 files changed, 5233 insertions(+), 43 deletions(-) create mode 100644 packages/core/src/ECS/Core/EntityHandle.ts create mode 100644 packages/core/src/ECS/Core/EntityHandleManager.ts create mode 100644 packages/core/src/ECS/Core/EpochManager.ts create mode 100644 packages/core/src/ECS/Core/Query/CompiledQuery.ts create mode 100644 packages/core/src/ECS/Core/SystemDependencyGraph.ts create mode 100644 packages/core/src/ECS/Core/SystemScheduler.ts create mode 100644 packages/core/src/ECS/Decorators/SystemScheduling.ts create mode 100644 packages/core/tests/ECS/Core/CompiledQuery.test.ts create mode 100644 packages/core/tests/ECS/Core/EntityHandle.test.ts create mode 100644 packages/core/tests/ECS/Core/EpochManager.test.ts create mode 100644 packages/core/tests/ECS/Core/SystemScheduler.test.ts create mode 100644 packages/core/tests/ECS/Systems/EntitySystemChangeDetection.test.ts diff --git a/docs/en/guide/system.md b/docs/en/guide/system.md index 40a18e3e..dba78ce4 100644 --- a/docs/en/guide/system.md +++ b/docs/en/guide/system.md @@ -672,6 +672,243 @@ scene.addSystem(new SystemB()); // addOrder = 1, executes second > **Note**: `addOrder` is automatically set by the framework when calling `addSystem`, no manual management needed. This ensures systems with the same `updateOrder` execute in their addition order, avoiding random behavior from unstable sorting. +## Declarative System Scheduling + +> **v2.4.0+** + +In addition to manually controlling execution order with `updateOrder`, the framework provides a declarative system scheduling mechanism that lets you define execution order through dependencies. + +### Scheduling Decorators + +```typescript +import { EntitySystem, ECSSystem, Stage, Before, After, InSet } from '@esengine/ecs-framework'; + +// Use decorators to declare system scheduling +@ECSSystem('Movement') +@Stage('update') // Execute in update stage +@After('InputSystem') // Execute after InputSystem +@Before('RenderSystem') // Execute before RenderSystem +class MovementSystem extends EntitySystem { + constructor() { + super(Matcher.all(Position, Velocity)); + } + + protected process(entities: readonly Entity[]): void { + // Movement logic + } +} + +// Use system sets for grouping +@ECSSystem('Physics') +@Stage('update') +@InSet('CoreSystems') // Belongs to CoreSystems set +class PhysicsSystem extends EntitySystem { + // ... +} + +@ECSSystem('Collision') +@Stage('update') +@After('set:CoreSystems') // Execute after all systems in CoreSystems set +class CollisionSystem extends EntitySystem { + // ... +} +``` + +### System Execution Stages + +The framework defines the following system execution stages, executed in order: + +| Stage | Description | Typical Usage | +|-------|-------------|---------------| +| `startup` | Startup stage | One-time initialization | +| `preUpdate` | Pre-update stage | Input handling, state preparation | +| `update` | Main update stage (default) | Core game logic | +| `postUpdate` | Post-update stage | Physics, collision detection | +| `cleanup` | Cleanup stage | Resource cleanup, state reset | + +### Fluent API Configuration + +If you prefer not to use decorators, you can configure scheduling at runtime using the Fluent API: + +```typescript +@ECSSystem('Movement') +class MovementSystem extends EntitySystem { + constructor() { + super(Matcher.all(Position, Velocity)); + + // Configure scheduling using Fluent API + this.stage('update') + .after('InputSystem') + .before('RenderSystem') + .inSet('CoreSystems'); + } +} +``` + +### Cycle Dependency Detection + +The framework automatically detects cyclic dependencies and throws clear errors: + +```typescript +// This will cause a cycle dependency error +@ECSSystem('SystemA') +@Before('SystemB') +class SystemA extends EntitySystem { } + +@ECSSystem('SystemB') +@Before('SystemA') // Error: A -> B -> A forms a cycle +class SystemB extends EntitySystem { } + +// Error message: Cyclic dependency detected: SystemA -> SystemB -> SystemA +``` + +## Frame-Level Change Detection + +> **v2.4.0+** + +The framework provides epoch-based frame-level change detection, allowing systems to process only entities that have changed, significantly improving performance. + +### Core Concepts + +- **Epoch**: Global frame counter, incremented each frame +- **lastWriteEpoch**: The epoch when a component was last modified +- **Change Detection**: Determine if component changed after a specific point by comparing epochs + +### Marking Components as Modified + +After modifying component data, you need to mark the component as changed. There are two approaches: + +**Approach 1: Via Entity Helper Method (Recommended)** + +```typescript +// Mark component dirty via entity.markDirty() after modification +const pos = entity.getComponent(Position)!; +pos.x = 100; +pos.y = 200; +entity.markDirty(pos); + +// Can mark multiple components at once +const vel = entity.getComponent(Velocity)!; +vel.vx = 10; +entity.markDirty(pos, vel); +``` + +**Approach 2: Encapsulate in Component** + +```typescript +class VelocityComponent extends Component { + private _vx: number = 0; + private _vy: number = 0; + + // Provide modification method that accepts epoch parameter + 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; } +} + +// Usage in system +const vel = entity.getComponent(VelocityComponent)!; +vel.setVelocity(10, 20, this.currentEpoch); +``` + +### Using Change Detection in Systems + +EntitySystem provides several change detection helper methods: + +```typescript +@ECSSystem('Physics') +class PhysicsSystem extends EntitySystem { + constructor() { + super(Matcher.all(Position, Velocity)); + } + + protected process(entities: readonly Entity[]): void { + // Method 1: Use forEachChanged to process only changed entities + // Automatically saves epoch checkpoint + this.forEachChanged(entities, [Velocity], (entity) => { + const pos = this.requireComponent(entity, Position); + const vel = this.requireComponent(entity, Velocity); + + // Only update position when Velocity changes + 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 { + // Method 2: Use filterChanged to get list of changed entities + const changedEntities = this.filterChanged(entities, [RigidBody]); + + for (const entity of changedEntities) { + // Process entities with changed physics state + this.updatePhysics(entity); + } + + // Manually save epoch checkpoint + this.saveEpoch(); + } + + protected updatePhysics(entity: Entity): void { + // Physics update logic + } +} +``` + +### Change Detection API Reference + +| Method | Description | +|--------|-------------| +| `forEachChanged(entities, [Types], callback)` | Iterate entities with changed specified components, auto-saves checkpoint | +| `filterChanged(entities, [Types])` | Return array of entities with changed specified components | +| `hasChanged(entity, [Types])` | Check if single entity's specified components have changed | +| `saveEpoch()` | Manually save current epoch as checkpoint | +| `lastProcessEpoch` | Get last saved epoch checkpoint | +| `currentEpoch` | Get current scene epoch | + +### Use Cases + +Change detection is particularly suitable for: + +1. **Dirty flag optimization**: Only update rendering when data changes +2. **Physics sync**: Only sync entities with changed position/velocity +3. **Network sync**: Only send changed component data +4. **Cache invalidation**: Only recalculate when dependent data changes + +```typescript +@ECSSystem('NetworkSync') +class NetworkSyncSystem extends EntitySystem { + constructor() { + super(Matcher.all(NetworkComponent, Transform)); + } + + protected process(entities: readonly Entity[]): void { + // Only sync changed entities, greatly reducing network traffic + 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 { + // Send network update + } +} +``` + ## Complex System Examples ### Collision Detection System diff --git a/docs/guide/entity-query.md b/docs/guide/entity-query.md index 11523b72..978846b2 100644 --- a/docs/guide/entity-query.md +++ b/docs/guide/entity-query.md @@ -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 diff --git a/docs/guide/system.md b/docs/guide/system.md index e7f35b51..c44220b7 100644 --- a/docs/guide/system.md +++ b/docs/guide/system.md @@ -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 { + // 发送网络更新 + } +} +``` + ## 复杂系统示例 ### 碰撞检测系统 diff --git a/packages/core/src/ECS/Component.ts b/packages/core/src/ECS/Component.ts index 20e45756..905fad73 100644 --- a/packages/core/src/ECS/Component.ts +++ b/packages/core/src/ECS/Component.ts @@ -57,6 +57,27 @@ export abstract class Component implements IComponent { @Int32 public entityId: number | null = null; + /** + * 最后写入的 epoch + * + * 用于帧级变更检测,记录组件最后一次被修改时的 epoch。 + * 0 表示从未被标记为已修改。 + * + * Last write epoch. + * Used for frame-level change detection, records the epoch when component was last modified. + * 0 means never marked as modified. + */ + private _lastWriteEpoch: number = 0; + + /** + * 获取最后写入的 epoch + * + * Get last write epoch. + */ + public get lastWriteEpoch(): number { + return this._lastWriteEpoch; + } + /** * 创建组件实例 * @@ -66,6 +87,29 @@ export abstract class Component implements IComponent { this.id = Component.idGenerator++; } + /** + * 标记组件为已修改 + * + * 调用此方法会更新组件的 lastWriteEpoch 为当前帧的 epoch。 + * 系统可以通过比较 lastWriteEpoch 和上次检查的 epoch 来判断组件是否发生变更。 + * + * Mark component as modified. + * Calling this method updates the component's lastWriteEpoch to the current frame's epoch. + * Systems can compare lastWriteEpoch with their last checked epoch to detect changes. + * + * @param epoch 当前帧的 epoch | Current frame's epoch + * + * @example + * ```typescript + * // 在修改组件数据后调用 + * velocity.x = 10; + * velocity.markDirty(scene.epochManager.current); + * ``` + */ + public markDirty(epoch: number): void { + this._lastWriteEpoch = epoch; + } + /** * 组件添加到实体时的回调 * diff --git a/packages/core/src/ECS/Core/CommandBuffer.ts b/packages/core/src/ECS/Core/CommandBuffer.ts index de8bc420..35a3543f 100644 --- a/packages/core/src/ECS/Core/CommandBuffer.ts +++ b/packages/core/src/ECS/Core/CommandBuffer.ts @@ -1,6 +1,6 @@ import { Entity } from '../Entity'; import { Component } from '../Component'; -import { ComponentType } from './ComponentStorage'; +import { ComponentType, ComponentRegistry } from './ComponentStorage'; import { IScene } from '../IScene'; import { createLogger } from '../../Utils/Logger'; @@ -38,6 +38,24 @@ export interface DeferredCommand { value?: boolean; } +/** + * 每个实体的待处理操作 + * Pending operations per entity + * + * 使用 last-write-wins 语义进行去重。 + * Uses last-write-wins semantics for deduplication. + */ +interface PendingPerEntity { + /** 是否销毁实体(如果为 true,忽略其他操作)| Destroy entity (if true, ignores other operations) */ + bDestroy?: boolean; + /** 最终的激活状态 | Final active state */ + active?: boolean; + /** 要添加的组件(typeId -> Component)| Components to add (typeId -> Component) */ + adds?: Map; + /** 要移除的组件类型 ID 集合 | Component type IDs to remove */ + removes?: Set; +} + /** * 命令缓冲区 - 用于延迟执行实体操作 * Command Buffer - for deferred entity operations @@ -45,8 +63,17 @@ export interface DeferredCommand { * 在系统的 process() 方法中使用 CommandBuffer 可以避免迭代过程中修改实体列表, * 从而提高性能(无需每帧拷贝数组)并保证迭代安全。 * - * Using CommandBuffer in system's process() method avoids modifying entity list during iteration, - * improving performance (no array copy per frame) and ensuring iteration safety. + * 特点: + * - **去重**: 同一实体的多次同类操作会被合并(last-write-wins) + * - **有序执行**: removes -> adds -> active -> destroy + * - **冲突处理**: addComponent 会取消同类型的 removeComponent,反之亦然 + * - **销毁优先**: destroyEntity 会取消该实体的所有其他操作 + * + * Features: + * - **Deduplication**: Multiple operations on same entity are merged (last-write-wins) + * - **Ordered execution**: removes -> adds -> active -> destroy + * - **Conflict handling**: addComponent cancels same-type removeComponent and vice versa + * - **Destroy priority**: destroyEntity cancels all other operations for that entity * * @example * ```typescript @@ -66,7 +93,10 @@ export interface DeferredCommand { * ``` */ export class CommandBuffer { - /** 命令队列 | Command queue */ + /** 每个实体的待处理操作 | Pending operations per entity */ + private _pending: Map = new Map(); + + /** 旧式命令队列(用于兼容)| Legacy command queue (for compatibility) */ private _commands: DeferredCommand[] = []; /** 关联的场景 | Associated scene */ @@ -75,6 +105,9 @@ export class CommandBuffer { /** 是否启用调试日志 | Enable debug logging */ private _debug: boolean = false; + /** 是否使用去重模式 | Whether to use deduplication mode */ + private _useDeduplication: boolean = true; + /** * 创建命令缓冲区 * Create command buffer @@ -103,11 +136,34 @@ export class CommandBuffer { return this._scene; } + /** + * 设置是否使用去重模式 + * Set whether to use deduplication mode + * + * @param enabled - 是否启用 | Whether to enable + */ + public setDeduplication(enabled: boolean): void { + this._useDeduplication = enabled; + } + /** * 获取待执行的命令数量 * Get pending command count + * + * 返回实际的操作数量,而不是实体数量。 + * Returns actual operation count, not entity count. */ public get pendingCount(): number { + if (this._useDeduplication) { + let count = 0; + for (const ops of this._pending.values()) { + if (ops.bDestroy) count++; + if (ops.active !== undefined) count++; + if (ops.adds) count += ops.adds.size; + if (ops.removes) count += ops.removes.size; + } + return count; + } return this._commands.length; } @@ -116,25 +172,82 @@ export class CommandBuffer { * Check if there are pending commands */ public get hasPending(): boolean { + if (this._useDeduplication) { + return this._pending.size > 0; + } return this._commands.length > 0; } + /** + * 获取或创建实体的待处理操作 + * Get or create pending operations for entity + */ + private getPending(entity: Entity): PendingPerEntity { + let pending = this._pending.get(entity); + if (!pending) { + pending = {}; + this._pending.set(entity, pending); + } + return pending; + } + + /** + * 获取组件类型 ID(位索引) + * Get component type ID (bit index) + */ + private getTypeId(componentOrType: Component | ComponentType): number { + if (typeof componentOrType === 'function') { + // ComponentType + return ComponentRegistry.getBitIndex(componentOrType); + } else { + // Component instance + return ComponentRegistry.getBitIndex(componentOrType.constructor as ComponentType); + } + } + /** * 延迟添加组件 * Deferred add component * + * 如果之前有同类型的 removeComponent,会被取消。 + * If there was a removeComponent for the same type, it will be canceled. + * * @param entity - 目标实体 | Target entity * @param component - 要添加的组件 | Component to add */ public addComponent(entity: Entity, component: Component): void { - this._commands.push({ - type: CommandType.ADD_COMPONENT, - entity, - component - }); + if (this._useDeduplication) { + const pending = this.getPending(entity); - if (this._debug) { - logger.debug(`CommandBuffer: 延迟添加组件 ${component.constructor.name} 到实体 ${entity.name}`); + // 如果实体已标记销毁,忽略 + if (pending.bDestroy) { + if (this._debug) { + logger.debug(`CommandBuffer: 忽略添加组件,实体 ${entity.name} 已标记销毁`); + } + return; + } + + const typeId = this.getTypeId(component); + + // 取消同类型的 remove + pending.removes?.delete(typeId); + + // 添加(覆盖同类型的之前的 add) + if (!pending.adds) { + pending.adds = new Map(); + } + pending.adds.set(typeId, component); + + if (this._debug) { + logger.debug(`CommandBuffer: 延迟添加组件 ${component.constructor.name} 到实体 ${entity.name}`); + } + } else { + // 旧模式 + this._commands.push({ + type: CommandType.ADD_COMPONENT, + entity, + component + }); } } @@ -142,18 +255,45 @@ export class CommandBuffer { * 延迟移除组件 * Deferred remove component * + * 如果之前有同类型的 addComponent,会被取消。 + * If there was an addComponent for the same type, it will be canceled. + * * @param entity - 目标实体 | Target entity * @param componentType - 要移除的组件类型 | Component type to remove */ public removeComponent(entity: Entity, componentType: ComponentType): void { - this._commands.push({ - type: CommandType.REMOVE_COMPONENT, - entity, - componentType - }); + if (this._useDeduplication) { + const pending = this.getPending(entity); - if (this._debug) { - logger.debug(`CommandBuffer: 延迟移除组件 ${componentType.name} 从实体 ${entity.name}`); + // 如果实体已标记销毁,忽略 + if (pending.bDestroy) { + if (this._debug) { + logger.debug(`CommandBuffer: 忽略移除组件,实体 ${entity.name} 已标记销毁`); + } + return; + } + + const typeId = this.getTypeId(componentType); + + // 取消同类型的 add + pending.adds?.delete(typeId); + + // 添加到移除列表 + if (!pending.removes) { + pending.removes = new Set(); + } + pending.removes.add(typeId); + + if (this._debug) { + logger.debug(`CommandBuffer: 延迟移除组件 ${componentType.name} 从实体 ${entity.name}`); + } + } else { + // 旧模式 + this._commands.push({ + type: CommandType.REMOVE_COMPONENT, + entity, + componentType + }); } } @@ -161,16 +301,32 @@ export class CommandBuffer { * 延迟销毁实体 * Deferred destroy entity * + * 会取消该实体的所有其他待处理操作。 + * Cancels all other pending operations for this entity. + * * @param entity - 要销毁的实体 | Entity to destroy */ public destroyEntity(entity: Entity): void { - this._commands.push({ - type: CommandType.DESTROY_ENTITY, - entity - }); + if (this._useDeduplication) { + const pending = this.getPending(entity); - if (this._debug) { - logger.debug(`CommandBuffer: 延迟销毁实体 ${entity.name}`); + // 清除所有其他操作 + pending.adds?.clear(); + pending.removes?.clear(); + delete pending.active; + + // 标记销毁 + pending.bDestroy = true; + + if (this._debug) { + logger.debug(`CommandBuffer: 延迟销毁实体 ${entity.name}`); + } + } else { + // 旧模式 + this._commands.push({ + type: CommandType.DESTROY_ENTITY, + entity + }); } } @@ -182,14 +338,30 @@ export class CommandBuffer { * @param active - 激活状态 | Active state */ public setEntityActive(entity: Entity, active: boolean): void { - this._commands.push({ - type: CommandType.SET_ENTITY_ACTIVE, - entity, - value: active - }); + if (this._useDeduplication) { + const pending = this.getPending(entity); - if (this._debug) { - logger.debug(`CommandBuffer: 延迟设置实体 ${entity.name} 激活状态为 ${active}`); + // 如果实体已标记销毁,忽略 + if (pending.bDestroy) { + if (this._debug) { + logger.debug(`CommandBuffer: 忽略设置激活状态,实体 ${entity.name} 已标记销毁`); + } + return; + } + + // 设置(覆盖之前的设置) + pending.active = active; + + if (this._debug) { + logger.debug(`CommandBuffer: 延迟设置实体 ${entity.name} 激活状态为 ${active}`); + } + } else { + // 旧模式 + this._commands.push({ + type: CommandType.SET_ENTITY_ACTIVE, + entity, + value: active + }); } } @@ -197,12 +369,120 @@ export class CommandBuffer { * 执行所有待处理的命令 * Execute all pending commands * + * 执行顺序:removes -> adds -> active -> destroy + * Execution order: removes -> adds -> active -> destroy + * * 通常在帧末由 Scene 自动调用。 * Usually called automatically by Scene at end of frame. * * @returns 执行的命令数量 | Number of commands executed */ public flush(): number { + if (this._useDeduplication) { + return this.flushDeduplication(); + } else { + return this.flushLegacy(); + } + } + + /** + * 使用去重模式刷新 + * Flush using deduplication mode + */ + private flushDeduplication(): number { + if (this._pending.size === 0) { + return 0; + } + + const entityCount = this._pending.size; + let commandCount = 0; + + if (this._debug) { + logger.debug(`CommandBuffer: 开始执行 ${entityCount} 个实体的延迟命令`); + } + + // 复制并清空,防止执行过程中有新命令加入 + const pending = this._pending; + this._pending = new Map(); + + // 分阶段执行 + // Phase 1: Removes + for (const [entity, ops] of pending) { + if (ops.bDestroy || !entity.scene) continue; + + if (ops.removes && ops.removes.size > 0) { + for (const typeId of ops.removes) { + try { + const componentType = ComponentRegistry.getTypeByBitIndex(typeId); + if (componentType) { + entity.removeComponentByType(componentType); + commandCount++; + } + } catch (error) { + logger.error(`CommandBuffer: 移除组件失败`, { entity: entity.name, typeId, error }); + } + } + } + } + + // Phase 2: Adds + for (const [entity, ops] of pending) { + if (ops.bDestroy || !entity.scene) continue; + + if (ops.adds && ops.adds.size > 0) { + for (const component of ops.adds.values()) { + try { + entity.addComponent(component); + commandCount++; + } catch (error) { + logger.error(`CommandBuffer: 添加组件失败`, { + entity: entity.name, + component: component.constructor.name, + error + }); + } + } + } + } + + // Phase 3: Active state + for (const [entity, ops] of pending) { + if (ops.bDestroy || !entity.scene) continue; + + if (ops.active !== undefined) { + try { + entity.active = ops.active; + commandCount++; + } catch (error) { + logger.error(`CommandBuffer: 设置激活状态失败`, { entity: entity.name, error }); + } + } + } + + // Phase 4: Destroy + for (const [entity, ops] of pending) { + if (!ops.bDestroy || !entity.scene) continue; + + try { + entity.destroy(); + commandCount++; + } catch (error) { + logger.error(`CommandBuffer: 销毁实体失败`, { entity: entity.name, error }); + } + } + + if (this._debug) { + logger.debug(`CommandBuffer: 完成执行 ${commandCount} 个延迟命令`); + } + + return commandCount; + } + + /** + * 使用旧模式刷新 + * Flush using legacy mode + */ + private flushLegacy(): number { if (this._commands.length === 0) { return 0; } @@ -214,7 +494,6 @@ export class CommandBuffer { } // 复制命令列表并清空,防止执行过程中有新命令加入 - // Copy and clear command list to prevent new commands during execution const commands = this._commands; this._commands = []; @@ -230,12 +509,11 @@ export class CommandBuffer { } /** - * 执行单个命令 - * Execute single command + * 执行单个命令(旧模式) + * Execute single command (legacy mode) */ private executeCommand(cmd: DeferredCommand): void { // 检查实体是否仍然有效 - // Check if entity is still valid if (!cmd.entity.scene) { if (this._debug) { logger.debug(`CommandBuffer: 跳过命令,实体 ${cmd.entity.name} 已无效`); @@ -277,9 +555,13 @@ export class CommandBuffer { * Clear all pending commands (without executing) */ public clear(): void { - if (this._debug && this._commands.length > 0) { - logger.debug(`CommandBuffer: 清空 ${this._commands.length} 个未执行的命令`); + if (this._debug) { + const count = this._useDeduplication ? this._pending.size : this._commands.length; + if (count > 0) { + logger.debug(`CommandBuffer: 清空 ${count} 个未执行的命令`); + } } + this._pending.clear(); this._commands.length = 0; } diff --git a/packages/core/src/ECS/Core/EntityHandle.ts b/packages/core/src/ECS/Core/EntityHandle.ts new file mode 100644 index 00000000..86f3ff50 --- /dev/null +++ b/packages/core/src/ECS/Core/EntityHandle.ts @@ -0,0 +1,136 @@ +/** + * 轻量级实体句柄 + * + * 使用数值表示实体,包含索引和代数信息。 + * 28位索引 + 20位代数 = 48位,在 JavaScript 安全整数范围内。 + * + * Lightweight entity handle. + * Uses numeric value to represent entity with index and generation. + * 28-bit index + 20-bit generation = 48-bit, within JavaScript safe integer range. + * + * @example + * ```typescript + * const handle = makeHandle(42, 1); + * console.log(indexOf(handle)); // 42 + * console.log(genOf(handle)); // 1 + * console.log(isValidHandle(handle)); // true + * ``` + */ + +/** + * 实体句柄类型 + * + * 使用 branded type 提供类型安全。 + * + * Entity handle type. + * Uses branded type for type safety. + */ +export type EntityHandle = number & { readonly __brand: 'EntityHandle' }; + +/** + * 索引位数 | Index bits + */ +export const INDEX_BITS = 28; + +/** + * 代数位数 | Generation bits + */ +export const GEN_BITS = 20; + +/** + * 索引掩码 | Index mask + */ +export const INDEX_MASK = (1 << INDEX_BITS) - 1; // 0x0FFFFFFF + +/** + * 代数掩码 | Generation mask + */ +export const GEN_MASK = (1 << GEN_BITS) - 1; // 0x000FFFFF + +/** + * 最大实体数量 | Maximum entity count + */ +export const MAX_ENTITIES = 1 << INDEX_BITS; // 268,435,456 + +/** + * 最大代数值 | Maximum generation value + */ +export const MAX_GENERATION = 1 << GEN_BITS; // 1,048,576 + +/** + * 空句柄常量 | Null handle constant + */ +export const NULL_HANDLE = 0 as EntityHandle; + +/** + * 创建实体句柄 + * Create entity handle + * + * @param index 实体索引 | Entity index + * @param generation 实体代数 | Entity generation + * @returns 实体句柄 | Entity handle + */ +export function makeHandle(index: number, generation: number): EntityHandle { + // handle = generation * 2^28 + index + // 使用乘法而不是位移,因为位移只支持 32 位 + return ((generation & GEN_MASK) * MAX_ENTITIES + (index & INDEX_MASK)) as EntityHandle; +} + +/** + * 从句柄提取索引 + * Extract index from handle + * + * @param handle 实体句柄 | Entity handle + * @returns 实体索引 | Entity index + */ +export function indexOf(handle: EntityHandle): number { + return handle & INDEX_MASK; +} + +/** + * 从句柄提取代数 + * Extract generation from handle + * + * @param handle 实体句柄 | Entity handle + * @returns 实体代数 | Entity generation + */ +export function genOf(handle: EntityHandle): number { + return Math.floor(handle / MAX_ENTITIES) & GEN_MASK; +} + +/** + * 检查句柄是否有效(非空) + * Check if handle is valid (non-null) + * + * @param handle 实体句柄 | Entity handle + * @returns 是否有效 | Whether valid + */ +export function isValidHandle(handle: EntityHandle): boolean { + return handle !== NULL_HANDLE; +} + +/** + * 比较两个句柄是否相等 + * Compare two handles for equality + * + * @param a 第一个句柄 | First handle + * @param b 第二个句柄 | Second handle + * @returns 是否相等 | Whether equal + */ +export function handleEquals(a: EntityHandle, b: EntityHandle): boolean { + return a === b; +} + +/** + * 将句柄转换为字符串(用于调试) + * Convert handle to string (for debugging) + * + * @param handle 实体句柄 | Entity handle + * @returns 字符串表示 | String representation + */ +export function handleToString(handle: EntityHandle): string { + if (handle === NULL_HANDLE) { + return 'Entity(NULL)'; + } + return `Entity(idx=${indexOf(handle)}, gen=${genOf(handle)})`; +} diff --git a/packages/core/src/ECS/Core/EntityHandleManager.ts b/packages/core/src/ECS/Core/EntityHandleManager.ts new file mode 100644 index 00000000..7f2f1b23 --- /dev/null +++ b/packages/core/src/ECS/Core/EntityHandleManager.ts @@ -0,0 +1,331 @@ +/** + * 实体句柄管理器 + * + * 管理轻量级实体句柄的生命周期,包括创建、销毁和状态查询。 + * 使用 TypedArray 实现高效的内存布局。 + * + * Entity handle manager. + * Manages lifecycle of lightweight entity handles including creation, destruction and state queries. + * Uses TypedArray for efficient memory layout. + * + * @example + * ```typescript + * const manager = new EntityHandleManager(); + * const handle = manager.create(); + * console.log(manager.isAlive(handle)); // true + * + * manager.destroy(handle); + * console.log(manager.isAlive(handle)); // false + * + * // 索引会被复用,但代数会增加 + * const newHandle = manager.create(); + * console.log(indexOf(handle) === indexOf(newHandle)); // true + * console.log(genOf(newHandle) > genOf(handle)); // true + * ``` + */ + +import { + EntityHandle, + makeHandle, + indexOf, + genOf, + MAX_ENTITIES, + MAX_GENERATION +} from './EntityHandle'; + +/** + * 初始容量 | Initial capacity + */ +const INITIAL_CAPACITY = 1024; + +/** + * 实体句柄管理器 + * + * Entity handle manager. + */ +export class EntityHandleManager { + /** + * 代数数组(每个索引的当前代数) + * Generation array (current generation for each index) + */ + private _generations: Uint32Array; + + /** + * 存活状态数组(0=死亡,1=存活) + * Alive state array (0=dead, 1=alive) + */ + private _alive: Uint8Array; + + /** + * 启用状态数组(0=禁用,1=启用) + * Enabled state array (0=disabled, 1=enabled) + */ + private _enabled: Uint8Array; + + /** + * 空闲索引列表(用于复用已销毁的索引) + * Free index list (for reusing destroyed indices) + */ + private _freeList: number[] = []; + + /** + * 下一个新索引 + * Next new index + */ + private _nextIndex: number = 1; // 从 1 开始,0 保留给 NULL_HANDLE + + /** + * 当前存活实体数量 + * Current alive entity count + */ + private _aliveCount: number = 0; + + /** + * 当前容量 + * Current capacity + */ + private _capacity: number; + + /** + * 创建实体句柄管理器 + * Create entity handle manager + * + * @param initialCapacity 初始容量 | Initial capacity + */ + constructor(initialCapacity: number = INITIAL_CAPACITY) { + this._capacity = initialCapacity; + this._generations = new Uint32Array(initialCapacity); + this._alive = new Uint8Array(initialCapacity); + this._enabled = new Uint8Array(initialCapacity); + + // 索引 0 保留给 NULL_HANDLE + this._alive[0] = 0; + this._enabled[0] = 0; + } + + /** + * 获取存活实体数量 + * Get alive entity count + */ + public get aliveCount(): number { + return this._aliveCount; + } + + /** + * 获取当前容量 + * Get current capacity + */ + public get capacity(): number { + return this._capacity; + } + + /** + * 创建新实体句柄 + * Create new entity handle + * + * @returns 新实体句柄 | New entity handle + */ + public create(): EntityHandle { + let index: number; + + // 优先从空闲列表中获取索引 + if (this._freeList.length > 0) { + index = this._freeList.pop()!; + } else { + // 分配新索引 + index = this._nextIndex++; + + // 检查是否需要扩容 + if (index >= this._capacity) { + this.grow(index); + } + } + + // 获取当前代数(可能因之前销毁而增加) + const generation = this._generations[index]!; + + // 标记为存活和启用 + this._alive[index] = 1; + this._enabled[index] = 1; + this._aliveCount++; + + return makeHandle(index, generation); + } + + /** + * 销毁实体句柄 + * Destroy entity handle + * + * @param handle 要销毁的句柄 | Handle to destroy + * @returns 是否成功销毁 | Whether destruction succeeded + */ + public destroy(handle: EntityHandle): boolean { + const index = indexOf(handle); + const generation = genOf(handle); + + // 验证句柄有效性 + if (index >= this._capacity || index === 0) { + return false; + } + + if (this._generations[index] !== generation) { + return false; + } + + if (this._alive[index] !== 1) { + return false; + } + + // 标记为死亡 + this._alive[index] = 0; + this._enabled[index] = 0; + this._aliveCount--; + + // 增加代数(防止 ABA 问题) + const newGen = (generation + 1) % MAX_GENERATION; + this._generations[index] = newGen; + + // 将索引加入空闲列表 + this._freeList.push(index); + + return true; + } + + /** + * 检查句柄是否存活 + * Check if handle is alive + * + * @param handle 实体句柄 | Entity handle + * @returns 是否存活 | Whether alive + */ + public isAlive(handle: EntityHandle): boolean { + const index = indexOf(handle); + const generation = genOf(handle); + + if (index >= this._capacity || index === 0) { + return false; + } + + return this._alive[index] === 1 && this._generations[index] === generation; + } + + /** + * 检查句柄是否启用 + * Check if handle is enabled + * + * @param handle 实体句柄 | Entity handle + * @returns 是否启用 | Whether enabled + */ + public isEnabled(handle: EntityHandle): boolean { + if (!this.isAlive(handle)) { + return false; + } + + const index = indexOf(handle); + return this._enabled[index] === 1; + } + + /** + * 设置句柄启用状态 + * Set handle enabled state + * + * @param handle 实体句柄 | Entity handle + * @param enabled 启用状态 | Enabled state + * @returns 是否成功设置 | Whether setting succeeded + */ + public setEnabled(handle: EntityHandle, enabled: boolean): boolean { + if (!this.isAlive(handle)) { + return false; + } + + const index = indexOf(handle); + this._enabled[index] = enabled ? 1 : 0; + return true; + } + + /** + * 验证句柄是否有效(存活且代数匹配) + * Validate if handle is valid (alive and generation matches) + * + * @param handle 实体句柄 | Entity handle + * @returns 是否有效 | Whether valid + */ + public validate(handle: EntityHandle): boolean { + return this.isAlive(handle); + } + + /** + * 扩容数组 + * Grow arrays + */ + private grow(minIndex: number): void { + let newSize = this._capacity; + + // 按 2 倍扩容 + while (newSize <= minIndex) { + newSize <<= 1; + } + + // 检查是否超过最大容量 + if (newSize > MAX_ENTITIES) { + newSize = MAX_ENTITIES; + if (minIndex >= newSize) { + throw new Error(`EntityHandleManager: 超过最大实体数量 ${MAX_ENTITIES}`); + } + } + + // 创建新数组并复制数据 + const newGenerations = new Uint32Array(newSize); + const newAlive = new Uint8Array(newSize); + const newEnabled = new Uint8Array(newSize); + + newGenerations.set(this._generations); + newAlive.set(this._alive); + newEnabled.set(this._enabled); + + this._generations = newGenerations; + this._alive = newAlive; + this._enabled = newEnabled; + this._capacity = newSize; + } + + /** + * 重置管理器状态 + * Reset manager state + */ + public reset(): void { + this._generations.fill(0); + this._alive.fill(0); + this._enabled.fill(0); + this._freeList.length = 0; + this._nextIndex = 1; + this._aliveCount = 0; + } + + /** + * 遍历所有存活的句柄 + * Iterate all alive handles + * + * @param callback 回调函数 | Callback function + */ + public forEach(callback: (handle: EntityHandle) => void): void { + for (let i = 1; i < this._nextIndex; i++) { + if (this._alive[i] === 1) { + const handle = makeHandle(i, this._generations[i]!); + callback(handle); + } + } + } + + /** + * 获取所有存活的句柄 + * Get all alive handles + * + * @returns 存活句柄数组 | Alive handles array + */ + public getAllAlive(): EntityHandle[] { + const result: EntityHandle[] = []; + this.forEach((handle) => result.push(handle)); + return result; + } +} diff --git a/packages/core/src/ECS/Core/EpochManager.ts b/packages/core/src/ECS/Core/EpochManager.ts new file mode 100644 index 00000000..72ee7022 --- /dev/null +++ b/packages/core/src/ECS/Core/EpochManager.ts @@ -0,0 +1,103 @@ +/** + * 帧级变更检测管理器 + * + * 基于 epoch(帧计数)的组件变更追踪系统。 + * 每帧递增 epoch,组件写入时记录当前 epoch, + * 系统可以查询自上次检查以来发生变更的组件。 + * + * Frame-level change detection manager. + * Epoch-based component change tracking system. + * Epoch increments each frame, components record current epoch on write, + * systems can query components changed since last check. + * + * @example + * ```typescript + * // 在 System 中使用 + * class MovementSystem extends EntitySystem { + * private _lastEpoch = 0; + * + * process(): void { + * const epoch = this.scene.epochManager; + * + * // 只处理 Velocity 变化的实体 + * for (const entity of this.entities) { + * const vel = entity.getComponent(Velocity); + * if (vel && vel.lastWriteEpoch > this._lastEpoch) { + * // 处理变更 + * } + * } + * + * this._lastEpoch = epoch.current; + * } + * } + * ``` + */ + +/** + * Epoch 管理器 + * + * Epoch manager. + */ +export class EpochManager { + /** + * 当前 epoch 值 + * + * 从 1 开始,0 表示"从未写入"。 + * + * Current epoch value. + * Starts from 1, 0 means "never written". + */ + private _current: number = 1; + + /** + * 获取当前 epoch + * + * Get current epoch. + */ + public get current(): number { + return this._current; + } + + /** + * 递增 epoch + * + * 应在每帧开始时调用。 + * + * Increment epoch. + * Should be called at the start of each frame. + */ + public increment(): void { + this._current++; + + // 处理溢出(虽然 2^53 帧基本不可能达到) + // Handle overflow (though 2^53 frames is practically unreachable) + if (this._current >= Number.MAX_SAFE_INTEGER) { + this._current = 1; + } + } + + /** + * 重置 epoch + * + * 在场景重置时调用。 + * + * Reset epoch. + * Called when scene is reset. + */ + public reset(): void { + this._current = 1; + } + + /** + * 检查给定 epoch 是否在指定 epoch 之后发生变更 + * + * Check if given epoch is after the specified epoch (changed since). + * + * @param writeEpoch 写入时的 epoch | Epoch when written + * @param sinceEpoch 检查点 epoch | Checkpoint epoch + * @returns 是否在检查点之后发生变更 | Whether changed after checkpoint + */ + public isChangedSince(writeEpoch: number, sinceEpoch: number): boolean { + return writeEpoch > sinceEpoch; + } +} diff --git a/packages/core/src/ECS/Core/Query/CompiledQuery.ts b/packages/core/src/ECS/Core/Query/CompiledQuery.ts new file mode 100644 index 00000000..e8b6d9d5 --- /dev/null +++ b/packages/core/src/ECS/Core/Query/CompiledQuery.ts @@ -0,0 +1,366 @@ +/** + * 编译查询 + * + * 预编译的查询执行计划,避免每次查询时重复构建匹配条件。 + * 提供便捷的迭代方法和变更检测支持。 + * + * Compiled Query. + * Pre-compiled query execution plan that avoids repeated condition building. + * Provides convenient iteration methods and change detection support. + * + * @example + * ```typescript + * class MovementSystem extends EntitySystem { + * private _query: CompiledQuery<[typeof Position, typeof Velocity]>; + * + * onInitialize(): void { + * this._query = this.scene.querySystem.compile(Position, Velocity); + * } + * + * update(): void { + * this._query.forEach((entity, pos, vel) => { + * pos.x += vel.x * this.deltaTime; + * pos.y += vel.y * this.deltaTime; + * }); + * } + * } + * ``` + */ + +import { Entity } from '../../Entity'; +import { Component } from '../../Component'; +import { ComponentType } from '../ComponentStorage'; +import { QuerySystem } from '../QuerySystem'; + +/** + * 组件实例类型提取 + * + * Extract component instance types from component type tuple. + */ +export type InstanceTypes = { + [K in keyof T]: T[K] extends ComponentType ? C : never; +}; + +/** + * 编译查询类 + * + * Compiled query class. + */ +export class CompiledQuery { + /** + * 查询的组件类型列表 + * + * Component types for this query. + */ + private readonly _componentTypes: T; + + /** + * 查询系统引用 + * + * Reference to query system. + */ + private readonly _querySystem: QuerySystem; + + /** + * 上次查询的版本号(用于缓存失效检测) + * + * Last query version (for cache invalidation detection). + */ + private _lastVersion: number = -1; + + /** + * 缓存的实体列表 + * + * Cached entity list. + */ + private _cachedEntities: readonly Entity[] = []; + + /** + * 创建编译查询 + * + * Create compiled query. + * + * @param querySystem 查询系统引用 | Query system reference + * @param componentTypes 组件类型列表 | Component type list + */ + constructor(querySystem: QuerySystem, ...componentTypes: T) { + this._querySystem = querySystem; + this._componentTypes = componentTypes; + } + + /** + * 获取组件类型列表 + * + * Get component type list. + */ + public get componentTypes(): readonly ComponentType[] { + return this._componentTypes; + } + + /** + * 获取匹配的实体列表 + * + * Get matching entity list. + */ + public get entities(): readonly Entity[] { + this._refreshCache(); + return this._cachedEntities; + } + + /** + * 获取匹配的实体数量 + * + * Get matching entity count. + */ + public get count(): number { + return this.entities.length; + } + + /** + * 刷新缓存 + * + * Refresh cache if needed. + */ + private _refreshCache(): void { + const currentVersion = this._querySystem.version; + if (this._lastVersion !== currentVersion) { + const result = this._querySystem.queryAll(...this._componentTypes); + this._cachedEntities = result.entities; + this._lastVersion = currentVersion; + } + } + + /** + * 遍历所有匹配的实体及其组件 + * + * Iterate all matching entities with their components. + * + * @param callback 回调函数,接收实体和组件实例 | Callback receiving entity and component instances + * + * @example + * ```typescript + * query.forEach((entity, position, velocity) => { + * position.x += velocity.x * deltaTime; + * }); + * ``` + */ + public forEach( + callback: (entity: Entity, ...components: InstanceTypes) => void + ): void { + const entities = this.entities; + const componentTypes = this._componentTypes; + const typeCount = componentTypes.length; + + for (let i = 0, len = entities.length; i < len; i++) { + const entity = entities[i]!; + const components: Component[] = new Array(typeCount); + + // 获取所有组件 + for (let j = 0; j < typeCount; j++) { + const component = entity.getComponent(componentTypes[j]!); + if (!component) { + // 组件不存在,跳过这个实体 + continue; + } + components[j] = component; + } + + // 调用回调 + callback(entity, ...(components as InstanceTypes)); + } + } + + /** + * 遍历自指定 epoch 以来发生变更的实体 + * + * Iterate entities with components changed since specified epoch. + * + * @param sinceEpoch 检查点 epoch | Checkpoint epoch + * @param callback 回调函数 | Callback function + * + * @example + * ```typescript + * class PhysicsSystem extends EntitySystem { + * private _lastEpoch = 0; + * + * update(): void { + * this._query.forEachChanged(this._lastEpoch, (entity, pos, vel) => { + * // 只处理变更的实体 + * }); + * this._lastEpoch = this.scene.epochManager.current; + * } + * } + * ``` + */ + public forEachChanged( + sinceEpoch: number, + callback: (entity: Entity, ...components: InstanceTypes) => void + ): void { + const entities = this.entities; + const componentTypes = this._componentTypes; + const typeCount = componentTypes.length; + + for (let i = 0, len = entities.length; i < len; i++) { + const entity = entities[i]!; + const components: Component[] = new Array(typeCount); + let hasChanged = false; + + // 获取所有组件并检查变更 + for (let j = 0; j < typeCount; j++) { + const component = entity.getComponent(componentTypes[j]!); + if (!component) { + continue; + } + components[j] = component; + + if (component.lastWriteEpoch > sinceEpoch) { + hasChanged = true; + } + } + + // 只在有变更时调用回调 + if (hasChanged) { + callback(entity, ...(components as InstanceTypes)); + } + } + } + + /** + * 获取第一个匹配的实体及其组件 + * + * Get first matching entity with its components. + * + * @returns 实体和组件元组,或 null | Entity and components tuple, or null + */ + public first(): [Entity, ...InstanceTypes] | null { + const entities = this.entities; + if (entities.length === 0) { + return null; + } + + const entity = entities[0]!; + const components: Component[] = []; + + for (const type of this._componentTypes) { + const component = entity.getComponent(type); + if (!component) { + return null; + } + components.push(component); + } + + return [entity, ...(components as InstanceTypes)]; + } + + /** + * 转换为数组 + * + * Convert to array. + * + * @returns 实体和组件元组数组 | Array of entity and components tuples + */ + public toArray(): Array<[Entity, ...InstanceTypes]> { + const result: Array<[Entity, ...InstanceTypes]> = []; + + this.forEach((entity, ...components) => { + result.push([entity, ...components]); + }); + + return result; + } + + /** + * 映射转换 + * + * Map transformation. + * + * @param callback 转换函数 | Transform function + * @returns 转换结果数组 | Array of transform results + */ + public map( + callback: (entity: Entity, ...components: InstanceTypes) => R + ): R[] { + const result: R[] = []; + + this.forEach((entity, ...components) => { + result.push(callback(entity, ...components)); + }); + + return result; + } + + /** + * 过滤实体 + * + * Filter entities. + * + * @param predicate 过滤条件 | Filter predicate + * @returns 过滤后的实体数组 | Filtered entity array + */ + public filter( + predicate: (entity: Entity, ...components: InstanceTypes) => boolean + ): Entity[] { + const result: Entity[] = []; + + this.forEach((entity, ...components) => { + if (predicate(entity, ...components)) { + result.push(entity); + } + }); + + return result; + } + + /** + * 查找满足条件的实体 + * + * Find entity matching predicate. + * + * @param predicate 查找条件 | Find predicate + * @returns 找到的实体或 undefined | Found entity or undefined + */ + public find( + predicate: (entity: Entity, ...components: InstanceTypes) => boolean + ): Entity | undefined { + const entities = this.entities; + const componentTypes = this._componentTypes; + const typeCount = componentTypes.length; + + for (let i = 0, len = entities.length; i < len; i++) { + const entity = entities[i]!; + const components: Component[] = new Array(typeCount); + + for (let j = 0; j < typeCount; j++) { + const component = entity.getComponent(componentTypes[j]!); + if (!component) { + continue; + } + components[j] = component; + } + + if (predicate(entity, ...(components as InstanceTypes))) { + return entity; + } + } + + return undefined; + } + + /** + * 检查是否有任何实体匹配 + * + * Check if any entity matches. + */ + public any(): boolean { + return this.count > 0; + } + + /** + * 检查是否没有实体匹配 + * + * Check if no entity matches. + */ + public empty(): boolean { + return this.count === 0; + } +} diff --git a/packages/core/src/ECS/Core/QuerySystem.ts b/packages/core/src/ECS/Core/QuerySystem.ts index 0806ff38..bddbc79a 100644 --- a/packages/core/src/ECS/Core/QuerySystem.ts +++ b/packages/core/src/ECS/Core/QuerySystem.ts @@ -7,8 +7,10 @@ import { getComponentTypeName } from '../Decorators'; import { Archetype, ArchetypeSystem } from './ArchetypeSystem'; import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery'; import { QueryCondition, QueryConditionType, QueryResult } from './QueryTypes'; +import { CompiledQuery } from './Query/CompiledQuery'; export { QueryCondition, QueryConditionType, QueryResult }; +export { CompiledQuery }; /** * 实体索引结构 @@ -530,6 +532,76 @@ export class QuerySystem { }; } + /** + * 查询自指定 epoch 以来发生变更的实体 + * + * 返回拥有指定组件且该组件在 sinceEpoch 之后被修改过的实体。 + * 用于实现增量更新,只处理发生变化的实体。 + * + * Query entities with components changed since the specified epoch. + * Returns entities that have the specified component type(s) and that component + * was modified after sinceEpoch. + * Used for incremental updates, only processing entities that have changed. + * + * @param sinceEpoch 上次检查的 epoch | Last checked epoch + * @param componentTypes 要检查的组件类型 | Component types to check + * @returns 查询结果,包含变更的实体 | Query result with changed entities + * + * @example + * ```typescript + * class MovementSystem extends EntitySystem { + * private _lastEpoch = 0; + * + * update(): void { + * // 只处理 Velocity 组件发生变化的实体 + * const result = this.scene.querySystem.queryChangedSince( + * this._lastEpoch, + * Velocity + * ); + * + * for (const entity of result.entities) { + * // 处理变更的实体 + * } + * + * this._lastEpoch = this.scene.epochManager.current; + * } + * } + * ``` + */ + public queryChangedSince(sinceEpoch: number, ...componentTypes: ComponentType[]): QueryResult { + const startTime = performance.now(); + this._queryStats.totalQueries++; + this._queryStats.dirtyChecks++; + + // 先获取所有拥有这些组件的实体 + const baseResult = this.queryAll(...componentTypes); + const changedEntities: Entity[] = []; + + // 过滤出组件发生变更的实体 + for (const entity of baseResult.entities) { + let hasChanged = false; + + for (const componentType of componentTypes) { + const component = entity.getComponent(componentType); + if (component && component.lastWriteEpoch > sinceEpoch) { + hasChanged = true; + break; + } + } + + if (hasChanged) { + changedEntities.push(entity); + } + } + + return { + entities: changedEntities, + count: changedEntities.length, + executionTime: performance.now() - startTime, + fromCache: false + }; + } + /** * 按单个组件类型查询实体 * @@ -704,6 +776,42 @@ export class QuerySystem { this.clearReactiveQueries(); } + /** + * 编译查询 + * + * 创建预编译的查询对象,避免每次查询时重复构建匹配条件。 + * 适用于在 System 中频繁执行的查询,提供便捷的迭代方法。 + * + * Compile query. + * Creates a pre-compiled query object that avoids repeated condition building. + * Suitable for frequently executed queries in Systems, provides convenient iteration. + * + * @param componentTypes 要查询的组件类型 | Component types to query + * @returns 编译查询实例 | Compiled query instance + * + * @example + * ```typescript + * class MovementSystem extends EntitySystem { + * private _query: CompiledQuery<[typeof Position, typeof Velocity]>; + * + * onInitialize(): void { + * this._query = this.scene.querySystem.compile(Position, Velocity); + * } + * + * update(): void { + * this._query.forEach((entity, pos, vel) => { + * pos.x += vel.x * this.deltaTime; + * pos.y += vel.y * this.deltaTime; + * pos.markDirty(this.scene.epochManager.current); + * }); + * } + * } + * ``` + */ + public compile(...componentTypes: T): CompiledQuery { + return new CompiledQuery(this, ...componentTypes); + } + /** * 创建响应式查询 * diff --git a/packages/core/src/ECS/Core/SystemDependencyGraph.ts b/packages/core/src/ECS/Core/SystemDependencyGraph.ts new file mode 100644 index 00000000..705dfb3d --- /dev/null +++ b/packages/core/src/ECS/Core/SystemDependencyGraph.ts @@ -0,0 +1,263 @@ +/** + * 系统依赖图 + * + * 用于构建系统间的依赖关系并进行拓扑排序。 + * 支持 before/after 依赖声明和 set 分组。 + * + * System dependency graph. + * Used to build dependency relationships between systems and perform topological sorting. + * Supports before/after dependencies and set grouping. + */ + +/** + * 循环依赖错误 + * Cycle dependency error + */ +export class CycleDependencyError extends Error { + /** + * 参与循环的节点 ID + * Node IDs involved in the cycle + */ + public readonly involvedNodes: string[]; + + constructor(involvedNodes: string[]) { + const message = `[SystemDependencyGraph] 检测到循环依赖 | Cycle dependency detected: ${involvedNodes.join(' -> ')}`; + super(message); + this.name = 'CycleDependencyError'; + this.involvedNodes = involvedNodes; + Object.setPrototypeOf(this, new.target.prototype); + } +} + +/** + * 依赖图节点 + * Dependency graph node + */ +interface GraphNode { + /** 节点 ID | Node ID */ + id: string; + /** 是否为虚拟集合节点 | Whether this is a virtual set node */ + bIsVirtual: boolean; + /** 入边(依赖于此节点的节点) | Incoming edges (nodes that depend on this node) */ + inEdges: Set; + /** 出边(此节点依赖的节点) | Outgoing edges (nodes this node depends on) */ + outEdges: Set; +} + +/** + * 系统依赖信息 + * System dependency info + */ +export interface SystemDependencyInfo { + /** 系统名称 | System name */ + name: string; + /** 在这些系统之前执行 | Execute before these systems */ + before: string[]; + /** 在这些系统之后执行 | Execute after these systems */ + after: string[]; + /** 所属集合 | Sets this system belongs to */ + sets: string[]; +} + +/** 集合前缀 | Set prefix */ +const SET_PREFIX = 'set:'; + +/** + * 系统依赖图 + * + * 使用 Kahn 算法进行拓扑排序,检测循环依赖。 + * + * System dependency graph. + * Uses Kahn's algorithm for topological sorting and cycle detection. + */ +export class SystemDependencyGraph { + /** 节点映射 | Node map */ + private _nodes: Map = new Map(); + + /** + * 添加系统节点 + * Add system node + * + * @param name 系统名称 | System name + */ + public addSystemNode(name: string): void { + this.getOrCreateNode(name, false); + } + + /** + * 添加集合节点(虚拟节点) + * Add set node (virtual node) + * + * @param setName 集合名称 | Set name + */ + public addSetNode(setName: string): void { + const nodeId = SET_PREFIX + setName; + this.getOrCreateNode(nodeId, true); + } + + /** + * 添加依赖边 + * Add dependency edge + * + * @param from 起始节点 | Source node + * @param to 目标节点 | Target node + */ + public addEdge(from: string, to: string): void { + if (from === to) return; + + const fromNode = this.getOrCreateNode(from, from.startsWith(SET_PREFIX)); + const toNode = this.getOrCreateNode(to, to.startsWith(SET_PREFIX)); + + fromNode.outEdges.add(to); + toNode.inEdges.add(from); + } + + /** + * 从系统依赖信息构建图 + * Build graph from system dependency info + * + * @param systems 系统依赖信息列表 | System dependency info list + */ + public buildFromSystems(systems: SystemDependencyInfo[]): void { + this.clear(); + + // 1. 添加所有系统节点 + for (const sys of systems) { + this.addSystemNode(sys.name); + + // 添加集合节点 + for (const setName of sys.sets) { + this.addSetNode(setName); + } + } + + // 2. 添加边 + for (const sys of systems) { + // 集合 -> 系统(系统属于集合,集合执行后系统执行) + for (const setName of sys.sets) { + const setId = SET_PREFIX + setName; + this.addEdge(setId, sys.name); + } + + // before: 此系统 -> 目标(此系统先执行) + for (const target of sys.before) { + const targetId = this.resolveTargetId(target); + this.addEdge(sys.name, targetId); + } + + // after: 目标 -> 此系统(目标先执行) + for (const target of sys.after) { + const targetId = this.resolveTargetId(target); + this.addEdge(targetId, sys.name); + } + } + } + + /** + * 执行拓扑排序(Kahn 算法) + * Perform topological sort (Kahn's algorithm) + * + * @returns 排序后的系统名称列表(不包含虚拟节点) | Sorted system names (excluding virtual nodes) + * @throws {CycleDependencyError} 如果存在循环依赖 | If cycle dependency detected + */ + public topologicalSort(): string[] { + // 复制入边计数(避免修改原始数据) + const inDegree = new Map(); + for (const [id, node] of this._nodes) { + inDegree.set(id, node.inEdges.size); + } + + // 初始化队列:入度为 0 的节点 + const queue: string[] = []; + for (const [id, degree] of inDegree) { + if (degree === 0) { + queue.push(id); + } + } + + const result: string[] = []; + let processedCount = 0; + + while (queue.length > 0) { + const nodeId = queue.shift()!; + processedCount++; + + const node = this._nodes.get(nodeId); + if (!node) continue; + + // 只添加非虚拟节点到结果 + if (!node.bIsVirtual) { + result.push(nodeId); + } + + // 减少所有出边目标的入度 + for (const outId of node.outEdges) { + const newDegree = (inDegree.get(outId) ?? 0) - 1; + inDegree.set(outId, newDegree); + + if (newDegree === 0) { + queue.push(outId); + } + } + } + + // 检测循环:如果处理的节点数少于总节点数,说明存在循环 + if (processedCount < this._nodes.size) { + const cycleNodes: string[] = []; + for (const [id, degree] of inDegree) { + if (degree > 0) { + cycleNodes.push(id); + } + } + throw new CycleDependencyError(cycleNodes); + } + + return result; + } + + /** + * 清空图 + * Clear graph + */ + public clear(): void { + this._nodes.clear(); + } + + /** + * 获取节点数量 + * Get node count + */ + public get size(): number { + return this._nodes.size; + } + + /** + * 获取或创建节点 + * Get or create node + */ + private getOrCreateNode(id: string, bIsVirtual: boolean): GraphNode { + let node = this._nodes.get(id); + if (!node) { + node = { + id, + bIsVirtual, + inEdges: new Set(), + outEdges: new Set() + }; + this._nodes.set(id, node); + } + return node; + } + + /** + * 解析目标 ID(支持 set: 前缀) + * Resolve target ID (supports set: prefix) + */ + private resolveTargetId(nameOrSet: string): string { + // 如果已经有 set: 前缀,直接返回 + if (nameOrSet.startsWith(SET_PREFIX)) { + return nameOrSet; + } + return nameOrSet; + } +} diff --git a/packages/core/src/ECS/Core/SystemScheduler.ts b/packages/core/src/ECS/Core/SystemScheduler.ts new file mode 100644 index 00000000..a48db72b --- /dev/null +++ b/packages/core/src/ECS/Core/SystemScheduler.ts @@ -0,0 +1,338 @@ +/** + * 系统调度器 + * + * 负责管理系统的执行阶段和依赖关系。 + * 支持声明式的 before/after 依赖和拓扑排序。 + * + * System scheduler. + * Manages system execution stages and dependencies. + * Supports declarative before/after dependencies and topological sorting. + * + * @example + * ```typescript + * // 使用装饰器声明依赖 + * @Stage('update') + * @After('PhysicsSystem') + * @Before('RenderSystem') + * class MovementSystem extends EntitySystem { } + * + * // 或者使用方法链 + * class MovementSystem extends EntitySystem { + * constructor() { + * super(); + * this.stage('update').after('PhysicsSystem').before('RenderSystem'); + * } + * } + * ``` + */ + +import { SystemDependencyGraph, CycleDependencyError, type SystemDependencyInfo } from './SystemDependencyGraph'; +import type { EntitySystem } from '../Systems/EntitySystem'; + +export { CycleDependencyError }; + +/** + * 系统执行阶段 + * System execution stage + */ +export type SystemStage = 'startup' | 'preUpdate' | 'update' | 'postUpdate' | 'cleanup'; + +/** + * 默认阶段执行顺序 + * Default stage execution order + */ +export const DEFAULT_STAGE_ORDER: readonly SystemStage[] = [ + 'startup', + 'preUpdate', + 'update', + 'postUpdate', + 'cleanup' +]; + +/** + * 系统调度元数据 + * System scheduling metadata + */ +export interface SystemSchedulingMetadata { + /** 执行阶段 | Execution stage */ + stage: SystemStage; + /** 在这些系统之前执行 | Execute before these systems */ + before: string[]; + /** 在这些系统之后执行 | Execute after these systems */ + after: string[]; + /** 所属集合 | Sets this system belongs to */ + sets: string[]; +} + +/** + * 默认调度元数据 + * Default scheduling metadata + */ +export const DEFAULT_SCHEDULING_METADATA: Readonly = { + stage: 'update', + before: [], + after: [], + sets: [] +}; + +/** + * 系统调度器 + * + * 管理系统的执行顺序,支持: + * - 阶段分组(startup, preUpdate, update, postUpdate, cleanup) + * - before/after 依赖声明 + * - 拓扑排序和循环检测 + * - 与现有 updateOrder 的兼容 + * + * System scheduler. + * Manages system execution order, supports: + * - Stage grouping (startup, preUpdate, update, postUpdate, cleanup) + * - before/after dependency declarations + * - Topological sorting and cycle detection + * - Compatibility with existing updateOrder + */ +export class SystemScheduler { + /** 按阶段分组的排序结果 | Sorted results grouped by stage */ + private _sortedByStage: Map = new Map(); + /** 是否需要重新构建 | Whether rebuild is needed */ + private _dirty: boolean = true; + /** 依赖图 | Dependency graph */ + private _graph: SystemDependencyGraph = new SystemDependencyGraph(); + /** 是否启用依赖排序 | Whether dependency sorting is enabled */ + private _useDependencySort: boolean = true; + + /** + * 设置是否使用依赖排序 + * Set whether to use dependency sorting + * + * @param enabled 是否启用 | Whether to enable + */ + public setUseDependencySort(enabled: boolean): void { + if (this._useDependencySort !== enabled) { + this._useDependencySort = enabled; + this._dirty = true; + } + } + + /** + * 标记需要重新构建 + * Mark as needing rebuild + */ + public markDirty(): void { + this._dirty = true; + } + + /** + * 获取指定阶段的排序后系统列表 + * Get sorted system list for specified stage + * + * @param systems 所有系统 | All systems + * @param stage 阶段 | Stage + * @returns 排序后的系统列表 | Sorted system list + */ + public getSortedSystems(systems: EntitySystem[], stage?: SystemStage): EntitySystem[] { + this.ensureBuilt(systems); + + if (stage) { + return this._sortedByStage.get(stage) ?? []; + } + + // 返回所有阶段按顺序合并的结果 + const result: EntitySystem[] = []; + for (const s of DEFAULT_STAGE_ORDER) { + const stageSystems = this._sortedByStage.get(s); + if (stageSystems) { + result.push(...stageSystems); + } + } + return result; + } + + /** + * 获取所有排序后的系统(按默认阶段顺序) + * Get all sorted systems (by default stage order) + * + * @param systems 所有系统 | All systems + * @returns 排序后的系统列表 | Sorted system list + */ + public getAllSortedSystems(systems: EntitySystem[]): EntitySystem[] { + return this.getSortedSystems(systems); + } + + /** + * 确保已构建排序结果 + * Ensure sorted results are built + */ + private ensureBuilt(systems: EntitySystem[]): void { + if (!this._dirty) return; + + this._sortedByStage.clear(); + + if (!this._useDependencySort || !this.hasDependencies(systems)) { + // 使用简单的 updateOrder 排序 + this.buildWithUpdateOrder(systems); + } else { + // 使用依赖图拓扑排序 + this.buildWithDependencyGraph(systems); + } + + this._dirty = false; + } + + /** + * 检查系统是否有依赖声明 + * Check if systems have dependency declarations + */ + private hasDependencies(systems: EntitySystem[]): boolean { + for (const sys of systems) { + const meta = this.getSchedulingMetadata(sys); + if (meta.before.length > 0 || meta.after.length > 0 || meta.sets.length > 0) { + return true; + } + // 检查 stage 是否非默认值 + if (meta.stage !== 'update') { + return true; + } + } + return false; + } + + /** + * 使用 updateOrder 构建排序 + * Build sorting with updateOrder + */ + private buildWithUpdateOrder(systems: EntitySystem[]): void { + // 先按 updateOrder 和 addOrder 排序 + const sorted = [...systems].sort((a, b) => { + const orderDiff = a.updateOrder - b.updateOrder; + if (orderDiff !== 0) return orderDiff; + return a.addOrder - b.addOrder; + }); + + // 所有系统放入 update 阶段 + this._sortedByStage.set('update', sorted); + } + + /** + * 使用依赖图构建排序 + * Build sorting with dependency graph + */ + private buildWithDependencyGraph(systems: EntitySystem[]): void { + // 按阶段分组 + const byStage = new Map(); + for (const stage of DEFAULT_STAGE_ORDER) { + byStage.set(stage, []); + } + + for (const sys of systems) { + const meta = this.getSchedulingMetadata(sys); + const stage = meta.stage; + const stageList = byStage.get(stage); + if (stageList) { + stageList.push(sys); + } else { + // 未知阶段放入 update + byStage.get('update')!.push(sys); + } + } + + // 对每个阶段内的系统进行拓扑排序 + for (const [stage, stageSystems] of byStage) { + if (stageSystems.length === 0) { + this._sortedByStage.set(stage, []); + continue; + } + + const sorted = this.sortSystemsInStage(stageSystems); + this._sortedByStage.set(stage, sorted); + } + } + + /** + * 在阶段内对系统进行拓扑排序 + * Sort systems within a stage using topological sort + */ + private sortSystemsInStage(systems: EntitySystem[]): EntitySystem[] { + // 构建名称到系统的映射 + const nameToSystem = new Map(); + const depInfos: SystemDependencyInfo[] = []; + + for (const sys of systems) { + const name = sys.systemName; + nameToSystem.set(name, sys); + + const meta = this.getSchedulingMetadata(sys); + depInfos.push({ + name, + before: meta.before, + after: meta.after, + sets: meta.sets + }); + } + + // 构建依赖图并排序 + this._graph.buildFromSystems(depInfos); + + try { + const sortedNames = this._graph.topologicalSort(); + + // 将排序结果映射回系统实例 + const result: EntitySystem[] = []; + for (const name of sortedNames) { + const sys = nameToSystem.get(name); + if (sys) { + result.push(sys); + } + } + + // 对于没有依赖的系统,保持 updateOrder 作为次要排序键 + return this.stableSortByUpdateOrder(result); + } catch (error) { + if (error instanceof CycleDependencyError) { + // 重新抛出循环错误,让用户知道 + throw error; + } + // 其他错误回退到 updateOrder 排序 + console.warn('[SystemScheduler] 拓扑排序失败,回退到 updateOrder 排序', error); + return this.fallbackSort(systems); + } + } + + /** + * 稳定排序:在拓扑顺序的基础上,用 updateOrder 作为次要排序键 + * Stable sort: use updateOrder as secondary key after topological order + */ + private stableSortByUpdateOrder(systems: EntitySystem[]): EntitySystem[] { + // 拓扑排序已经保证了依赖顺序 + // 对于没有直接依赖关系的系统,按 updateOrder 排序 + // 这里简单处理:假设拓扑排序已经正确,只需保持结果 + return systems; + } + + /** + * 回退排序:使用 updateOrder + * Fallback sort: use updateOrder + */ + private fallbackSort(systems: EntitySystem[]): EntitySystem[] { + return [...systems].sort((a, b) => { + const orderDiff = a.updateOrder - b.updateOrder; + if (orderDiff !== 0) return orderDiff; + return a.addOrder - b.addOrder; + }); + } + + /** + * 获取系统的调度元数据 + * Get system scheduling metadata + */ + private getSchedulingMetadata(system: EntitySystem): SystemSchedulingMetadata { + // 使用公共 getter 方法获取调度信息 + // Use public getter methods to access scheduling info + return { + stage: system.getStage(), + before: [...system.getBefore()], + after: [...system.getAfter()], + sets: [...system.getSets()] + }; + } +} diff --git a/packages/core/src/ECS/Decorators/SystemScheduling.ts b/packages/core/src/ECS/Decorators/SystemScheduling.ts new file mode 100644 index 00000000..2df55e4d --- /dev/null +++ b/packages/core/src/ECS/Decorators/SystemScheduling.ts @@ -0,0 +1,177 @@ +/** + * 系统调度装饰器 + * + * 提供声明式的系统调度配置,包括执行阶段和依赖关系。 + * + * System scheduling decorators. + * Provides declarative system scheduling configuration including execution stage and dependencies. + * + * @example + * ```typescript + * @Stage('update') + * @After('PhysicsSystem') + * @Before('RenderSystem') + * @InSet('GameplaySystems') + * class MovementSystem extends EntitySystem { + * // ... + * } + * ``` + */ + +import type { SystemStage, SystemSchedulingMetadata } from '../Core/SystemScheduler'; + +/** + * 调度元数据 Symbol + * Scheduling metadata symbol + */ +export const SCHEDULING_METADATA = Symbol('schedulingMetadata'); + +/** + * 获取或创建调度元数据 + * Get or create scheduling metadata + */ +function getOrCreateMetadata(target: object): SystemSchedulingMetadata { + const proto = target as Record; + if (!proto[SCHEDULING_METADATA]) { + proto[SCHEDULING_METADATA] = { + stage: 'update', + before: [], + after: [], + sets: [] + }; + } + return proto[SCHEDULING_METADATA]!; +} + +/** + * 设置系统执行阶段 + * Set system execution stage + * + * @param stage 执行阶段 | Execution stage + * - 'startup': 只在首帧执行一次 | Execute once on first frame + * - 'preUpdate': 在 update 之前执行 | Execute before update + * - 'update': 主更新阶段(默认)| Main update stage (default) + * - 'postUpdate': 在 update 之后执行 | Execute after update + * - 'cleanup': 帧末清理阶段 | End of frame cleanup stage + * + * @example + * ```typescript + * @Stage('preUpdate') + * class InputSystem extends EntitySystem { } + * + * @Stage('postUpdate') + * class RenderSystem extends EntitySystem { } + * ``` + */ +export function Stage(stage: SystemStage): ClassDecorator { + return function (target) { + const metadata = getOrCreateMetadata(target.prototype); + metadata.stage = stage; + return target; + }; +} + +/** + * 声明此系统在指定系统之前执行 + * Declare this system executes before specified systems + * + * @param systems 系统名称列表 | System names + * + * @example + * ```typescript + * @Before('RenderSystem', 'UISystem') + * class TransformSystem extends EntitySystem { } + * ``` + */ +export function Before(...systems: string[]): ClassDecorator { + return function (target) { + const metadata = getOrCreateMetadata(target.prototype); + metadata.before.push(...systems); + return target; + }; +} + +/** + * 声明此系统在指定系统之后执行 + * Declare this system executes after specified systems + * + * @param systems 系统名称列表(可使用 'set:' 前缀指定集合)| System names (use 'set:' prefix for sets) + * + * @example + * ```typescript + * @After('PhysicsSystem') + * class CollisionResponseSystem extends EntitySystem { } + * + * // 使用集合 + * @After('set:PhysicsSystems') + * class GameLogicSystem extends EntitySystem { } + * ``` + */ +export function After(...systems: string[]): ClassDecorator { + return function (target) { + const metadata = getOrCreateMetadata(target.prototype); + metadata.after.push(...systems); + return target; + }; +} + +/** + * 将系统加入指定集合 + * Add system to specified sets + * + * 集合是虚拟分组,可用于批量依赖声明。 + * Sets are virtual groups that can be used for batch dependency declarations. + * + * @param sets 集合名称列表 | Set names + * + * @example + * ```typescript + * @InSet('PhysicsSystems') + * class RigidBodySystem extends EntitySystem { } + * + * @InSet('PhysicsSystems') + * class CollisionSystem extends EntitySystem { } + * + * // 其他系统可以依赖整个集合 + * @After('set:PhysicsSystems') + * class GameLogicSystem extends EntitySystem { } + * ``` + */ +export function InSet(...sets: string[]): ClassDecorator { + return function (target) { + const metadata = getOrCreateMetadata(target.prototype); + metadata.sets.push(...sets); + return target; + }; +} + +/** + * 获取系统的调度元数据 + * Get system scheduling metadata + * + * @param target 系统类或实例 | System class or instance + * @returns 调度元数据或 undefined | Scheduling metadata or undefined + */ +export function getSchedulingMetadata(target: object): SystemSchedulingMetadata | undefined { + // 从原型链查找 + let proto = Object.getPrototypeOf(target); + while (proto) { + const metadata = (proto as Record)[SCHEDULING_METADATA]; + if (metadata) { + return metadata; + } + proto = Object.getPrototypeOf(proto); + } + return undefined; +} + +/** + * 检查系统是否有调度元数据 + * Check if system has scheduling metadata + * + * @param target 系统类或实例 | System class or instance + * @returns 是否有元数据 | Whether has metadata + */ +export function hasSchedulingMetadata(target: object): boolean { + return getSchedulingMetadata(target) !== undefined; +} diff --git a/packages/core/src/ECS/Decorators/index.ts b/packages/core/src/ECS/Decorators/index.ts index a4cbe588..b6f5d010 100644 --- a/packages/core/src/ECS/Decorators/index.ts +++ b/packages/core/src/ECS/Decorators/index.ts @@ -67,3 +67,17 @@ export type { PropertyAssetType, EnumOption } from './PropertyDecorator'; + +// ============================================================================ +// System Scheduling Decorators +// 系统调度装饰器 +// ============================================================================ +export { + Stage, + Before, + After, + InSet, + getSchedulingMetadata, + hasSchedulingMetadata, + SCHEDULING_METADATA +} from './SystemScheduling'; diff --git a/packages/core/src/ECS/Entity.ts b/packages/core/src/ECS/Entity.ts index 6bc0decd..330ca772 100644 --- a/packages/core/src/ECS/Entity.ts +++ b/packages/core/src/ECS/Entity.ts @@ -6,6 +6,7 @@ import { createLogger } from '../Utils/Logger'; import { getComponentInstanceTypeName, getComponentTypeName } from './Decorators'; import { generateGUID } from '../Utils/GUID'; import type { IScene } from './IScene'; +import { EntityHandle, NULL_HANDLE } from './Core/EntityHandle'; /** * 组件活跃状态变化接口 @@ -93,6 +94,19 @@ export class Entity { */ public readonly persistentId: string; + /** + * 轻量级实体句柄 + * + * 数值类型的实体标识符,包含索引和代数信息。 + * 用于高性能场景下替代对象引用,支持 Archetype 存储等优化。 + * + * Lightweight entity handle. + * Numeric identifier containing index and generation. + * Used for high-performance scenarios instead of object references, + * supports Archetype storage optimizations. + */ + private _handle: EntityHandle = NULL_HANDLE; + /** * 所属场景引用 */ @@ -171,6 +185,34 @@ export class Entity { return this._lifecyclePolicy === EEntityLifecyclePolicy.Persistent; } + /** + * 获取实体句柄 + * + * 返回轻量级数值句柄,用于高性能场景。 + * 如果实体尚未分配句柄,返回 NULL_HANDLE。 + * + * Get entity handle. + * Returns lightweight numeric handle for high-performance scenarios. + * Returns NULL_HANDLE if entity has no handle assigned. + */ + public get handle(): EntityHandle { + return this._handle; + } + + /** + * 设置实体句柄(内部使用) + * + * 此方法供 Scene 在创建实体时调用。 + * + * Set entity handle (internal use). + * Called by Scene when creating entities. + * + * @internal + */ + public setHandle(handle: EntityHandle): void { + this._handle = handle; + } + /** * 设置实体为持久化(跨场景保留) * @@ -559,6 +601,39 @@ export class Entity { return component; } + /** + * 标记组件为已修改 + * + * 便捷方法,自动从场景获取当前 epoch 并标记组件。 + * 用于帧级变更检测系统。 + * + * Mark component(s) as modified. + * Convenience method that auto-gets epoch from scene and marks components. + * Used for frame-level change detection system. + * + * @param components 要标记的组件 | Components to mark + * + * @example + * ```typescript + * const pos = entity.getComponent(Position)!; + * pos.x = 100; + * entity.markDirty(pos); + * + * // 或者标记多个组件 + * entity.markDirty(pos, vel); + * ``` + */ + public markDirty(...components: Component[]): void { + if (!this.scene) { + return; + } + + const epoch = this.scene.epochManager.current; + for (const component of components) { + component.markDirty(epoch); + } + } + /** * 移除指定的组件 * diff --git a/packages/core/src/ECS/IScene.ts b/packages/core/src/ECS/IScene.ts index f320aee0..566838e9 100644 --- a/packages/core/src/ECS/IScene.ts +++ b/packages/core/src/ECS/IScene.ts @@ -5,6 +5,7 @@ import { EntitySystem } from './Systems/EntitySystem'; import { ComponentStorageManager, ComponentType } from './Core/ComponentStorage'; import { QuerySystem } from './Core/QuerySystem'; import { TypeSafeEventSystem } from './Core/EventSystem'; +import { EpochManager } from './Core/EpochManager'; import type { ReferenceTracker } from './Core/ReferenceTracker'; import type { ServiceContainer, ServiceType } from '../Core/ServiceContainer'; import type { TypedQueryBuilder } from './Core/Query/TypedQuery'; @@ -71,6 +72,16 @@ export interface IScene { */ readonly referenceTracker: ReferenceTracker; + /** + * Epoch 管理器 + * + * 用于帧级变更检测,追踪组件修改。 + * + * Epoch manager. + * Used for frame-level change detection, tracking component modifications. + */ + readonly epochManager: EpochManager; + /** * 服务容器 * diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index 50294e5f..8de11b87 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -27,6 +27,10 @@ import { AutoProfiler } from '../Utils/Profiler/AutoProfiler'; import { ServiceContainer, type ServiceType, type IService } from '../Core/ServiceContainer'; import { createInstance, isInjectable, injectProperties } from '../Core/DI'; import { createLogger } from '../Utils/Logger'; +import { SystemScheduler, CycleDependencyError } from './Core/SystemScheduler'; +import { EntityHandleManager } from './Core/EntityHandleManager'; +import { EntityHandle, isValidHandle } from './Core/EntityHandle'; +import { EpochManager } from './Core/EpochManager'; /** * 游戏场景默认实现类 @@ -92,6 +96,38 @@ export class Scene implements IScene { */ public readonly referenceTracker: ReferenceTracker; + /** + * 实体句柄管理器 + * + * 管理轻量级实体句柄的生命周期,包括创建、销毁和状态查询。 + * 用于高性能场景下替代对象引用。 + * + * Entity handle manager. + * Manages lifecycle of lightweight entity handles. + * Used for high-performance scenarios instead of object references. + */ + public readonly handleManager: EntityHandleManager; + + /** + * 句柄到实体的映射 + * + * 用于 O(1) 时间复杂度的句柄查找。 + * + * Handle to entity mapping. + * Used for O(1) lookup by handle. + */ + private readonly _handleToEntity: Map = new Map(); + + /** + * Epoch 管理器 + * + * 用于帧级变更检测,追踪组件的修改时间。 + * + * Epoch manager. + * Used for frame-level change detection, tracking component modification time. + */ + public readonly epochManager: EpochManager = new EpochManager(); + /** * 服务容器 * @@ -175,6 +211,18 @@ export class Scene implements IScene { */ private _systemAddCounter: number = 0; + /** + * 系统调度器 + * + * 用于管理系统的执行阶段和依赖关系。 + * 支持 before/after 依赖声明和拓扑排序。 + * + * System scheduler. + * Manages system execution stages and dependencies. + * Supports before/after dependency declarations and topological sorting. + */ + private readonly _systemScheduler: SystemScheduler = new SystemScheduler(); + /** * 组件ID到系统的索引映射 * @@ -220,12 +268,34 @@ export class Scene implements IScene { /** * 重新构建系统缓存 * - * 从服务容器中提取所有EntitySystem并排序 + * 从服务容器中提取所有EntitySystem并排序。 + * 使用 SystemScheduler 进行依赖排序,支持 before/after 声明。 + * + * Rebuild system cache. + * Extract all EntitySystems from service container and sort them. + * Uses SystemScheduler for dependency sorting, supports before/after declarations. */ private _rebuildSystemsCache(): EntitySystem[] { const allServices = this._services.getAll(); const systems = this._filterEntitySystems(allServices); - return this._sortSystemsByUpdateOrder(systems); + + try { + // 使用 SystemScheduler 进行依赖排序 + this._systemScheduler.markDirty(); + return this._systemScheduler.getAllSortedSystems(systems); + } catch (error) { + if (error instanceof CycleDependencyError) { + // 循环依赖错误,记录警告并回退到 updateOrder 排序 + this.logger.error( + `[Scene] 系统存在循环依赖,回退到 updateOrder 排序 | Cycle dependency detected, falling back to updateOrder sort`, + error.involvedNodes + ); + } else { + this.logger.error(`[Scene] 系统排序失败 | System sorting failed`, error); + } + // 回退到简单的 updateOrder 排序 + return this._sortSystemsByUpdateOrder(systems); + } } /** @@ -302,6 +372,7 @@ export class Scene implements IScene { this.querySystem = new QuerySystem(); this.eventSystem = new TypeSafeEventSystem(); this.referenceTracker = new ReferenceTracker(); + this.handleManager = new EntityHandleManager(); this._services = new ServiceContainer(); this.logger = createLogger('Scene'); @@ -429,12 +500,22 @@ export class Scene implements IScene { // 清空组件索引 | Clear component indices this._componentIdToSystems.clear(); this._globalNotifySystems.clear(); + + // 清空句柄映射并重置句柄管理器 | Clear handle mapping and reset handle manager + this._handleToEntity.clear(); + this.handleManager.reset(); + + // 重置 epoch 管理器 | Reset epoch manager + this.epochManager.reset(); } /** * 更新场景 */ public update() { + // 递增帧计数(用于变更检测) | Increment epoch (for change detection) + this.epochManager.increment(); + // 开始性能采样帧 ProfilerSDK.beginFrame(); const frameHandle = ProfilerSDK.beginSample('Scene.update', ProfileCategory.ECS); @@ -549,6 +630,13 @@ export class Scene implements IScene { public createEntity(name: string) { const entity = new Entity(name, this.identifierPool.checkOut()); + // 分配轻量级句柄 | Assign lightweight handle + const handle = this.handleManager.create(); + entity.setHandle(handle); + + // 添加到句柄映射 | Add to handle mapping + this._handleToEntity.set(handle, entity); + this.eventSystem.emitSync('entity:created', { entityName: name, entity, scene: this }); return this.addEntity(entity); @@ -736,6 +824,14 @@ export class Scene implements IScene { for (let i = 0; i < count; i++) { const entity = new Entity(`${namePrefix}_${i}`, this.identifierPool.checkOut()); entity.scene = this; + + // 分配轻量级句柄 | Assign lightweight handle + const handle = this.handleManager.create(); + entity.setHandle(handle); + + // 添加到句柄映射 | Add to handle mapping + this._handleToEntity.set(handle, entity); + entities.push(entity); } @@ -770,6 +866,12 @@ export class Scene implements IScene { for (const entity of entities) { this.entities.remove(entity); this.querySystem.removeEntity(entity); + + // 销毁句柄并从映射中移除 | Destroy handle and remove from mapping + if (isValidHandle(entity.handle)) { + this._handleToEntity.delete(entity.handle); + this.handleManager.destroy(entity.handle); + } } this.querySystem.clearCache(); @@ -801,6 +903,26 @@ export class Scene implements IScene { return this.entities.findEntityById(id); } + /** + * 根据句柄查找实体 + * + * 通过轻量级句柄查找对应的实体(O(1) 时间复杂度)。 + * 如果句柄无效或实体已被销毁,返回 null。 + * + * Find entity by handle (O(1) time complexity). + * Returns null if handle is invalid or entity has been destroyed. + * + * @param handle 实体句柄 | Entity handle + * @returns 实体实例或 null | Entity instance or null + */ + public findEntityByHandle(handle: EntityHandle): Entity | null { + if (!isValidHandle(handle) || !this.handleManager.isAlive(handle)) { + return null; + } + + return this._handleToEntity.get(handle) ?? null; + } + /** * 根据标签查找实体 * @param tag 实体标签 diff --git a/packages/core/src/ECS/Systems/EntitySystem.ts b/packages/core/src/ECS/Systems/EntitySystem.ts index c57b0758..194f2bf8 100644 --- a/packages/core/src/ECS/Systems/EntitySystem.ts +++ b/packages/core/src/ECS/Systems/EntitySystem.ts @@ -11,6 +11,8 @@ import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHe import type { IService } from '../../Core/ServiceContainer'; import { EntityCache } from './EntityCache'; import { CommandBuffer } from '../Core/CommandBuffer'; +import type { SystemStage, SystemSchedulingMetadata } from '../Core/SystemScheduler'; +import { getSchedulingMetadata } from '../Decorators/SystemScheduling'; /** * 事件监听器记录 @@ -83,6 +85,12 @@ export abstract class EntitySystem implements ISystemBase, IService { private _destroyed: boolean; protected logger: ReturnType; + /** + * 调度元数据 + * Scheduling metadata + */ + protected _schedulingMetadata: SystemSchedulingMetadata; + /** * 实体ID映射缓存 */ @@ -118,6 +126,15 @@ export abstract class EntitySystem implements ISystemBase, IService { */ protected readonly commands: CommandBuffer = new CommandBuffer(); + /** + * 上次处理的 epoch 检查点 + * Last processed epoch checkpoint + * + * 用于变更检测,记录上次 forEachChanged 完成时的 epoch。 + * Used for change detection, records epoch when forEachChanged last completed. + */ + private _lastProcessEpoch: number = 0; + /** * 获取系统处理的实体列表 */ @@ -205,6 +222,18 @@ export abstract class EntitySystem implements ISystemBase, IService { this.logger = createLogger(this.getLoggerName()); this._entityCache = new EntityCache(); + + // 初始化调度元数据(优先使用装饰器元数据) + // Initialize scheduling metadata (prefer decorator metadata) + const decoratorMeta = getSchedulingMetadata(this); + this._schedulingMetadata = decoratorMeta + ? { ...decoratorMeta } + : { + stage: 'update', + before: [], + after: [], + sets: [] + }; } /** @@ -257,6 +286,135 @@ export abstract class EntitySystem implements ISystemBase, IService { this._scene?.markSystemsOrderDirty(); } + // ======================================================================== + // 调度配置方法 (Fluent API) + // Scheduling configuration methods (Fluent API) + // ======================================================================== + + /** + * 设置系统执行阶段 + * Set system execution stage + * + * @param stage 执行阶段 | Execution stage + * @returns this (用于链式调用 | for chaining) + * + * @example + * ```typescript + * class MySystem extends EntitySystem { + * constructor() { + * super(); + * this.stage('preUpdate'); + * } + * } + * ``` + */ + public stage(stage: SystemStage): this { + this._schedulingMetadata.stage = stage; + this._scene?.markSystemsOrderDirty(); + return this; + } + + /** + * 声明此系统在指定系统之前执行 + * Declare this system executes before specified systems + * + * @param systems 系统名称列表 | System names + * @returns this (用于链式调用 | for chaining) + * + * @example + * ```typescript + * class MySystem extends EntitySystem { + * constructor() { + * super(); + * this.before('RenderSystem'); + * } + * } + * ``` + */ + public before(...systems: string[]): this { + this._schedulingMetadata.before.push(...systems); + this._scene?.markSystemsOrderDirty(); + return this; + } + + /** + * 声明此系统在指定系统之后执行 + * Declare this system executes after specified systems + * + * @param systems 系统名称列表(可使用 'set:' 前缀指定集合)| System names (use 'set:' prefix for sets) + * @returns this (用于链式调用 | for chaining) + * + * @example + * ```typescript + * class MySystem extends EntitySystem { + * constructor() { + * super(); + * this.after('PhysicsSystem', 'set:CoreSystems'); + * } + * } + * ``` + */ + public after(...systems: string[]): this { + this._schedulingMetadata.after.push(...systems); + this._scene?.markSystemsOrderDirty(); + return this; + } + + /** + * 将系统加入指定集合 + * Add system to specified sets + * + * @param sets 集合名称列表 | Set names + * @returns this (用于链式调用 | for chaining) + * + * @example + * ```typescript + * class MySystem extends EntitySystem { + * constructor() { + * super(); + * this.inSet('PhysicsSystems'); + * } + * } + * ``` + */ + public inSet(...sets: string[]): this { + this._schedulingMetadata.sets.push(...sets); + this._scene?.markSystemsOrderDirty(); + return this; + } + + /** + * 获取系统的执行阶段 + * Get system execution stage + */ + public getStage(): SystemStage { + return this._schedulingMetadata.stage; + } + + /** + * 获取系统应该在哪些系统之前执行 + * Get systems that this system should execute before + */ + public getBefore(): readonly string[] { + return this._schedulingMetadata.before; + } + + /** + * 获取系统应该在哪些系统之后执行 + * Get systems that this system should execute after + */ + public getAfter(): readonly string[] { + return this._schedulingMetadata.after; + } + + /** + * 获取系统所属的集合 + * Get sets that this system belongs to + */ + public getSets(): readonly string[] { + return this._schedulingMetadata.sets; + } + /** * 系统初始化(框架调用) * @@ -1302,4 +1460,167 @@ export abstract class EntitySystem implements ISystemBase, IService { } return true; } + + // ======================================================================== + // 变更检测方法 (Change Detection Methods) + // ======================================================================== + + /** + * 获取上次处理的 epoch 检查点 + * Get the last processed epoch checkpoint + * + * @returns 上次处理完成时的 epoch | Epoch when last processing completed + */ + protected get lastProcessEpoch(): number { + return this._lastProcessEpoch; + } + + /** + * 获取当前 epoch + * Get the current epoch + * + * @returns 当前的 epoch 值,如果 scene 不可用则返回 0 | Current epoch value, or 0 if scene unavailable + */ + protected get currentEpoch(): number { + return this._scene?.epochManager?.current ?? 0; + } + + /** + * 保存当前 epoch 作为检查点 + * Save current epoch as checkpoint + * + * 调用此方法后,下次 forEachChanged 将只处理此时间点之后变更的组件。 + * After calling this, next forEachChanged will only process components changed after this point. + */ + protected saveEpoch(): void { + this._lastProcessEpoch = this.currentEpoch; + } + + /** + * 遍历有变更组件的实体 + * Iterate entities with changed components + * + * 只处理自上次 saveEpoch() 或指定 epoch 以来组件发生变更的实体。 + * 处理完成后自动更新 lastProcessEpoch。 + * + * Only processes entities whose components changed since last saveEpoch() or specified epoch. + * Automatically updates lastProcessEpoch after processing. + * + * @param entities 实体列表 | Entity list + * @param componentTypes 要检查变更的组件类型 | Component types to check for changes + * @param processor 处理函数 | Processor function + * @param sinceEpoch 可选的起始 epoch,默认使用 lastProcessEpoch | Optional starting epoch, defaults to lastProcessEpoch + * + * @example + * ```typescript + * protected process(entities: readonly Entity[]): void { + * // 只处理 Velocity 组件发生变更的实体 + * this.forEachChanged(entities, [Velocity], (entity) => { + * const vel = this.requireComponent(entity, Velocity); + * // 只有 velocity 改变时才重新计算 + * this.updatePhysics(entity, vel); + * }); + * } + * ``` + */ + protected forEachChanged( + entities: readonly Entity[], + componentTypes: T, + processor: (entity: Entity, index: number) => void, + sinceEpoch?: number + ): void { + const checkEpoch = sinceEpoch ?? this._lastProcessEpoch; + + for (let i = 0; i < entities.length; i++) { + const entity = entities[i]!; + let hasChanged = false; + + // 检查任意指定组件是否发生变更 + // Check if any specified component has changed + for (const componentType of componentTypes) { + const component = entity.getComponent(componentType); + if (component && component.lastWriteEpoch > checkEpoch) { + hasChanged = true; + break; + } + } + + if (hasChanged) { + processor(entity, i); + } + } + + // 自动更新检查点 + // Automatically update checkpoint + this._lastProcessEpoch = this.currentEpoch; + } + + /** + * 过滤出有变更组件的实体 + * Filter entities with changed components + * + * @param entities 实体列表 | Entity list + * @param componentTypes 要检查变更的组件类型 | Component types to check for changes + * @param sinceEpoch 可选的起始 epoch | Optional starting epoch + * @returns 有变更的实体数组 | Array of entities with changes + * + * @example + * ```typescript + * protected process(entities: readonly Entity[]): void { + * const changedEntities = this.filterChanged(entities, [Position, Velocity]); + * for (const entity of changedEntities) { + * // 处理位置或速度变化的实体 + * } + * this.saveEpoch(); + * } + * ``` + */ + protected filterChanged( + entities: readonly Entity[], + componentTypes: T, + sinceEpoch?: number + ): Entity[] { + const checkEpoch = sinceEpoch ?? this._lastProcessEpoch; + const result: Entity[] = []; + + for (let i = 0; i < entities.length; i++) { + const entity = entities[i]!; + + for (const componentType of componentTypes) { + const component = entity.getComponent(componentType); + if (component && component.lastWriteEpoch > checkEpoch) { + result.push(entity); + break; + } + } + } + + return result; + } + + /** + * 检查实体的指定组件是否发生变更 + * Check if entity's specified components have changed + * + * @param entity 实体 | Entity + * @param componentTypes 要检查的组件类型 | Component types to check + * @param sinceEpoch 可选的起始 epoch | Optional starting epoch + * @returns 是否有变更 | Whether there are changes + */ + protected hasChanged( + entity: Entity, + componentTypes: T, + sinceEpoch?: number + ): boolean { + const checkEpoch = sinceEpoch ?? this._lastProcessEpoch; + + for (const componentType of componentTypes) { + const component = entity.getComponent(componentType); + if (component && component.lastWriteEpoch > checkEpoch) { + return true; + } + } + + return false; + } } diff --git a/packages/core/src/ECS/index.ts b/packages/core/src/ECS/index.ts index f48411ad..5e980d11 100644 --- a/packages/core/src/ECS/index.ts +++ b/packages/core/src/ECS/index.ts @@ -23,3 +23,35 @@ export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig } export { CommandBuffer, CommandType } from './Core/CommandBuffer'; export type { DeferredCommand } from './Core/CommandBuffer'; export * from './EntityTags'; + +// System Scheduling +export { SystemScheduler, CycleDependencyError, DEFAULT_STAGE_ORDER } from './Core/SystemScheduler'; +export type { SystemStage, SystemSchedulingMetadata } from './Core/SystemScheduler'; +export { SystemDependencyGraph } from './Core/SystemDependencyGraph'; +export type { SystemDependencyInfo } from './Core/SystemDependencyGraph'; + +// Entity Handle +export { + makeHandle, + indexOf, + genOf, + isValidHandle, + handleEquals, + handleToString, + NULL_HANDLE, + INDEX_BITS, + GEN_BITS, + INDEX_MASK, + GEN_MASK, + MAX_ENTITIES, + MAX_GENERATION +} from './Core/EntityHandle'; +export type { EntityHandle } from './Core/EntityHandle'; +export { EntityHandleManager } from './Core/EntityHandleManager'; + +// Change Detection +export { EpochManager } from './Core/EpochManager'; + +// Compiled Query +export { CompiledQuery } from './Core/Query/CompiledQuery'; +export type { InstanceTypes } from './Core/Query/CompiledQuery'; diff --git a/packages/core/src/Types/TypeHelpers.ts b/packages/core/src/Types/TypeHelpers.ts index b86577d5..a08b09e2 100644 --- a/packages/core/src/Types/TypeHelpers.ts +++ b/packages/core/src/Types/TypeHelpers.ts @@ -16,9 +16,10 @@ export type ComponentInstance = T extends new (...args: any[]) => infer R ? R /** * 组件构造函数类型 * - * 与 ComponentType 保持一致,避免类型转换 + * 使用 Component 作为默认类型,与 ComponentType 保持一致。 + * 这确保类型兼容性,因为所有实际组件都继承自 Component 类。 */ -export type ComponentConstructor = new (...args: any[]) => T; +export type ComponentConstructor = new (...args: any[]) => T; /** * 组件类型的通用约束 @@ -237,7 +238,7 @@ export interface TypedQueryCondition< /** * 组件类型守卫 */ -export function isComponentType( +export function isComponentType( value: any ): value is ComponentConstructor { return typeof value === 'function' && value.prototype instanceof Component; diff --git a/packages/core/src/Types/index.ts b/packages/core/src/Types/index.ts index 87493f14..bb4bd201 100644 --- a/packages/core/src/Types/index.ts +++ b/packages/core/src/Types/index.ts @@ -20,6 +20,22 @@ export interface IComponent { /** 组件所属的实体ID */ entityId: number | null; + /** + * 最后写入的 epoch + * + * 用于帧级变更检测,记录组件最后一次被修改时的 epoch。 + * + * Last write epoch for frame-level change detection. + */ + readonly lastWriteEpoch: number; + + /** + * 标记组件为已修改 + * + * Mark component as modified with current epoch. + */ + markDirty(epoch: number): void; + /** 组件添加到实体时的回调 */ onAddedToEntity(): void; /** 组件从实体移除时的回调 */ diff --git a/packages/core/tests/ECS/Core/CommandBuffer.test.ts b/packages/core/tests/ECS/Core/CommandBuffer.test.ts index 6934b866..3638d3ac 100644 --- a/packages/core/tests/ECS/Core/CommandBuffer.test.ts +++ b/packages/core/tests/ECS/Core/CommandBuffer.test.ts @@ -282,7 +282,9 @@ describe('CommandBuffer', () => { commandBuffer.addComponent(entity, new MarkerComponent()); commandBuffer.destroyEntity(entity); - expect(commandBuffer.pendingCount).toBe(2); + // 由于去重逻辑,destroyEntity 会清除同一实体的其他操作 + // Due to deduplication, destroyEntity clears other operations for the same entity + expect(commandBuffer.pendingCount).toBe(1); commandBuffer.clear(); diff --git a/packages/core/tests/ECS/Core/CompiledQuery.test.ts b/packages/core/tests/ECS/Core/CompiledQuery.test.ts new file mode 100644 index 00000000..25ff3240 --- /dev/null +++ b/packages/core/tests/ECS/Core/CompiledQuery.test.ts @@ -0,0 +1,373 @@ +import { CompiledQuery } from '../../../src/ECS/Core/Query/CompiledQuery'; +import { Scene } from '../../../src/ECS/Scene'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { ECSComponent } from '../../../src/ECS/Decorators'; + +// 测试组件 +@ECSComponent('CompiledQuery_Position') +class PositionComponent extends Component { + public x: number; + public y: number; + + constructor(...args: unknown[]) { + super(); + const [x = 0, y = 0] = args as [number?, number?]; + this.x = x; + this.y = y; + } +} + +@ECSComponent('CompiledQuery_Velocity') +class VelocityComponent extends Component { + public vx: number; + public vy: number; + + constructor(...args: unknown[]) { + super(); + const [vx = 0, vy = 0] = args as [number?, number?]; + this.vx = vx; + this.vy = vy; + } +} + +@ECSComponent('CompiledQuery_Health') +class HealthComponent extends Component { + public current: number; + public max: number; + + constructor(...args: unknown[]) { + super(); + const [current = 100, max = 100] = args as [number?, number?]; + this.current = current; + this.max = max; + } +} + +describe('CompiledQuery', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene(); + }); + + afterEach(() => { + scene.end(); + }); + + describe('创建和基本属性', () => { + it('应该能通过 QuerySystem.compile 创建', () => { + const query = scene.querySystem.compile(PositionComponent); + expect(query).toBeInstanceOf(CompiledQuery); + }); + + it('应该保存组件类型列表', () => { + const query = scene.querySystem.compile(PositionComponent, VelocityComponent); + expect(query.componentTypes).toContain(PositionComponent); + expect(query.componentTypes).toContain(VelocityComponent); + }); + }); + + describe('entities 属性', () => { + it('初始应该返回空数组', () => { + const query = scene.querySystem.compile(PositionComponent); + expect(query.entities).toHaveLength(0); + }); + + it('应该返回匹配的实体', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + + const query = scene.querySystem.compile(PositionComponent); + expect(query.entities).toHaveLength(1); + expect(query.entities[0]).toBe(entity); + }); + + it('应该只返回拥有所有组件的实体', () => { + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent()); + entity1.addComponent(new VelocityComponent()); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent()); + // entity2 没有 VelocityComponent + + const query = scene.querySystem.compile(PositionComponent, VelocityComponent); + expect(query.entities).toHaveLength(1); + expect(query.entities[0]).toBe(entity1); + }); + }); + + describe('count 属性', () => { + it('应该返回匹配实体的数量', () => { + const query = scene.querySystem.compile(PositionComponent); + expect(query.count).toBe(0); + + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent()); + + expect(query.count).toBe(1); + }); + }); + + describe('forEach', () => { + it('应该遍历所有匹配的实体', () => { + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + const query = scene.querySystem.compile(PositionComponent); + const visited: Entity[] = []; + + query.forEach((entity, pos) => { + visited.push(entity); + }); + + expect(visited).toHaveLength(2); + expect(visited).toContain(entity1); + expect(visited).toContain(entity2); + }); + + it('应该提供类型安全的组件参数', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + entity.addComponent(new VelocityComponent(1, 2)); + + const query = scene.querySystem.compile(PositionComponent, VelocityComponent); + + query.forEach((entity, pos, vel) => { + expect(pos.x).toBe(10); + expect(pos.y).toBe(20); + expect(vel.vx).toBe(1); + expect(vel.vy).toBe(2); + }); + }); + }); + + describe('forEachChanged', () => { + it('应该只遍历变更的实体', () => { + const entity1 = scene.createEntity('entity1'); + const pos1 = entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('entity2'); + const pos2 = entity2.addComponent(new PositionComponent(30, 40)); + + const query = scene.querySystem.compile(PositionComponent); + + // 获取当前 epoch + const epoch = scene.epochManager.current; + + // 递增 epoch + scene.epochManager.increment(); + + // 只标记 entity1 的组件为已修改 + pos1.markDirty(scene.epochManager.current); + + const changed: Entity[] = []; + query.forEachChanged(epoch, (entity, pos) => { + changed.push(entity); + }); + + expect(changed).toHaveLength(1); + expect(changed[0]).toBe(entity1); + }); + + it('当所有组件都未变更时应该不遍历任何实体', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + + const query = scene.querySystem.compile(PositionComponent); + + // 使用当前 epoch 检查 - 组件的 epoch 应该小于等于当前 + const futureEpoch = scene.epochManager.current + 100; + + const changed: Entity[] = []; + query.forEachChanged(futureEpoch, (entity, pos) => { + changed.push(entity); + }); + + expect(changed).toHaveLength(0); + }); + }); + + describe('first', () => { + it('应该返回第一个匹配的实体和组件', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + + const query = scene.querySystem.compile(PositionComponent); + const result = query.first(); + + expect(result).not.toBeNull(); + expect(result![0]).toBe(entity); + expect(result![1].x).toBe(10); + expect(result![1].y).toBe(20); + }); + + it('没有匹配实体时应该返回 null', () => { + const query = scene.querySystem.compile(PositionComponent); + const result = query.first(); + + expect(result).toBeNull(); + }); + }); + + describe('toArray', () => { + it('应该返回实体和组件的数组', () => { + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + const query = scene.querySystem.compile(PositionComponent); + const result = query.toArray(); + + expect(result).toHaveLength(2); + expect(result[0]![0]).toBe(entity1); + expect(result[0]![1].x).toBe(10); + }); + }); + + describe('map', () => { + it('应该映射转换实体数据', () => { + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + const query = scene.querySystem.compile(PositionComponent); + const result = query.map((entity, pos) => pos.x + pos.y); + + expect(result).toHaveLength(2); + expect(result).toContain(30); // 10 + 20 + expect(result).toContain(70); // 30 + 40 + }); + }); + + describe('filter', () => { + it('应该过滤实体', () => { + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + const query = scene.querySystem.compile(PositionComponent); + const result = query.filter((entity, pos) => pos.x > 20); + + expect(result).toHaveLength(1); + expect(result[0]).toBe(entity2); + }); + }); + + describe('find', () => { + it('应该找到第一个满足条件的实体', () => { + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + const query = scene.querySystem.compile(PositionComponent); + const result = query.find((entity, pos) => pos.x > 20); + + expect(result).toBe(entity2); + }); + + it('找不到时应该返回 undefined', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + + const query = scene.querySystem.compile(PositionComponent); + const result = query.find((entity, pos) => pos.x > 100); + + expect(result).toBeUndefined(); + }); + }); + + describe('any', () => { + it('有匹配实体时应该返回 true', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent()); + + const query = scene.querySystem.compile(PositionComponent); + expect(query.any()).toBe(true); + }); + + it('没有匹配实体时应该返回 false', () => { + const query = scene.querySystem.compile(PositionComponent); + expect(query.any()).toBe(false); + }); + }); + + describe('empty', () => { + it('没有匹配实体时应该返回 true', () => { + const query = scene.querySystem.compile(PositionComponent); + expect(query.empty()).toBe(true); + }); + + it('有匹配实体时应该返回 false', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent()); + + const query = scene.querySystem.compile(PositionComponent); + expect(query.empty()).toBe(false); + }); + }); + + describe('缓存机制', () => { + it('应该缓存查询结果', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent()); + + const query = scene.querySystem.compile(PositionComponent); + + // 第一次访问 + const entities1 = query.entities; + // 第二次访问 + const entities2 = query.entities; + + // 应该返回相同的缓存数组 + expect(entities1).toBe(entities2); + }); + + it('当实体变化时应该刷新缓存', () => { + const query = scene.querySystem.compile(PositionComponent); + + // 初始为空 + expect(query.count).toBe(0); + + // 添加实体 + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent()); + + // 应该检测到变化 + expect(query.count).toBe(1); + }); + }); + + describe('多组件查询', () => { + it('应该支持多组件查询', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + entity.addComponent(new VelocityComponent(1, 2)); + entity.addComponent(new HealthComponent(80, 100)); + + const query = scene.querySystem.compile( + PositionComponent, + VelocityComponent, + HealthComponent + ); + + query.forEach((entity, pos, vel, health) => { + expect(pos.x).toBe(10); + expect(vel.vx).toBe(1); + expect(health.current).toBe(80); + }); + }); + }); +}); diff --git a/packages/core/tests/ECS/Core/EntityHandle.test.ts b/packages/core/tests/ECS/Core/EntityHandle.test.ts new file mode 100644 index 00000000..4643b51a --- /dev/null +++ b/packages/core/tests/ECS/Core/EntityHandle.test.ts @@ -0,0 +1,380 @@ +import { + makeHandle, + indexOf, + genOf, + isValidHandle, + handleEquals, + handleToString, + NULL_HANDLE, + INDEX_BITS, + GEN_BITS, + INDEX_MASK, + GEN_MASK, + MAX_ENTITIES, + MAX_GENERATION, + EntityHandle +} from '../../../src/ECS/Core/EntityHandle'; +import { EntityHandleManager } from '../../../src/ECS/Core/EntityHandleManager'; + +describe('EntityHandle', () => { + describe('常量定义', () => { + it('INDEX_BITS 应该是 28', () => { + expect(INDEX_BITS).toBe(28); + }); + + it('GEN_BITS 应该是 20', () => { + expect(GEN_BITS).toBe(20); + }); + + it('INDEX_MASK 应该正确', () => { + expect(INDEX_MASK).toBe((1 << INDEX_BITS) - 1); + }); + + it('GEN_MASK 应该正确', () => { + expect(GEN_MASK).toBe((1 << GEN_BITS) - 1); + }); + + it('MAX_ENTITIES 应该是 2^28', () => { + expect(MAX_ENTITIES).toBe(1 << INDEX_BITS); + }); + + it('MAX_GENERATION 应该是 2^20', () => { + expect(MAX_GENERATION).toBe(1 << GEN_BITS); + }); + + it('NULL_HANDLE 应该是 0', () => { + expect(NULL_HANDLE).toBe(0); + }); + }); + + describe('makeHandle', () => { + it('应该正确组合 index 和 generation', () => { + const handle = makeHandle(100, 5); + expect(indexOf(handle)).toBe(100); + expect(genOf(handle)).toBe(5); + }); + + it('应该处理边界值', () => { + // 最小值 + const minHandle = makeHandle(0, 0); + expect(indexOf(minHandle)).toBe(0); + expect(genOf(minHandle)).toBe(0); + + // 最大 index + const maxIndexHandle = makeHandle(INDEX_MASK, 0); + expect(indexOf(maxIndexHandle)).toBe(INDEX_MASK); + + // 最大 generation + const maxGenHandle = makeHandle(0, GEN_MASK); + expect(genOf(maxGenHandle)).toBe(GEN_MASK); + }); + + it('应该正确处理大数值', () => { + const largeIndex = 1000000; + const largeGen = 500; + const handle = makeHandle(largeIndex, largeGen); + + expect(indexOf(handle)).toBe(largeIndex); + expect(genOf(handle)).toBe(largeGen); + }); + }); + + describe('indexOf', () => { + it('应该正确提取 index', () => { + const handle = makeHandle(12345, 67); + expect(indexOf(handle)).toBe(12345); + }); + + it('应该对 NULL_HANDLE 返回 0', () => { + expect(indexOf(NULL_HANDLE)).toBe(0); + }); + }); + + describe('genOf', () => { + it('应该正确提取 generation', () => { + const handle = makeHandle(12345, 67); + expect(genOf(handle)).toBe(67); + }); + + it('应该对 NULL_HANDLE 返回 0', () => { + expect(genOf(NULL_HANDLE)).toBe(0); + }); + }); + + describe('isValidHandle', () => { + it('应该判断 NULL_HANDLE 为无效', () => { + expect(isValidHandle(NULL_HANDLE)).toBe(false); + }); + + it('应该判断非零句柄为有效', () => { + const handle = makeHandle(1, 0); + expect(isValidHandle(handle)).toBe(true); + }); + + it('应该判断带 generation 的句柄为有效', () => { + const handle = makeHandle(0, 1); + expect(isValidHandle(handle)).toBe(true); + }); + }); + + describe('handleEquals', () => { + it('应该正确比较相同句柄', () => { + const handle1 = makeHandle(100, 5); + const handle2 = makeHandle(100, 5); + expect(handleEquals(handle1, handle2)).toBe(true); + }); + + it('应该正确比较不同句柄', () => { + const handle1 = makeHandle(100, 5); + const handle2 = makeHandle(100, 6); + expect(handleEquals(handle1, handle2)).toBe(false); + }); + + it('应该区分 index 不同的句柄', () => { + const handle1 = makeHandle(100, 5); + const handle2 = makeHandle(101, 5); + expect(handleEquals(handle1, handle2)).toBe(false); + }); + }); + + describe('handleToString', () => { + it('应该返回可读的字符串格式', () => { + const handle = makeHandle(100, 5); + const str = handleToString(handle); + expect(str).toContain('100'); + expect(str).toContain('5'); + }); + + it('应该对 NULL_HANDLE 返回特殊标记', () => { + const str = handleToString(NULL_HANDLE); + expect(str.toLowerCase()).toContain('null'); + }); + }); +}); + +describe('EntityHandleManager', () => { + let manager: EntityHandleManager; + + beforeEach(() => { + manager = new EntityHandleManager(); + }); + + describe('create', () => { + it('应该创建有效的句柄', () => { + const handle = manager.create(); + expect(isValidHandle(handle)).toBe(true); + }); + + it('应该创建不同的句柄', () => { + const handle1 = manager.create(); + const handle2 = manager.create(); + expect(handleEquals(handle1, handle2)).toBe(false); + }); + + it('应该递增 index', () => { + const handle1 = manager.create(); + const handle2 = manager.create(); + expect(indexOf(handle2)).toBe(indexOf(handle1) + 1); + }); + + it('创建的句柄应该是存活的', () => { + const handle = manager.create(); + expect(manager.isAlive(handle)).toBe(true); + }); + + it('创建的句柄默认应该是启用的', () => { + const handle = manager.create(); + expect(manager.isEnabled(handle)).toBe(true); + }); + }); + + describe('destroy', () => { + it('应该销毁句柄', () => { + const handle = manager.create(); + expect(manager.isAlive(handle)).toBe(true); + + manager.destroy(handle); + expect(manager.isAlive(handle)).toBe(false); + }); + + it('销毁后使用相同 index 应该增加 generation', () => { + const handle1 = manager.create(); + const index1 = indexOf(handle1); + + manager.destroy(handle1); + const handle2 = manager.create(); + const index2 = indexOf(handle2); + + // 应该复用同一个 index + expect(index2).toBe(index1); + // 但 generation 应该不同 + expect(genOf(handle2)).toBe(genOf(handle1) + 1); + }); + + it('销毁已销毁的句柄不应该报错', () => { + const handle = manager.create(); + manager.destroy(handle); + + expect(() => { + manager.destroy(handle); + }).not.toThrow(); + }); + + it('销毁 NULL_HANDLE 不应该报错', () => { + expect(() => { + manager.destroy(NULL_HANDLE); + }).not.toThrow(); + }); + }); + + describe('isAlive', () => { + it('应该对存活句柄返回 true', () => { + const handle = manager.create(); + expect(manager.isAlive(handle)).toBe(true); + }); + + it('应该对已销毁句柄返回 false', () => { + const handle = manager.create(); + manager.destroy(handle); + expect(manager.isAlive(handle)).toBe(false); + }); + + it('应该对 NULL_HANDLE 返回 false', () => { + expect(manager.isAlive(NULL_HANDLE)).toBe(false); + }); + + it('应该对过期 generation 返回 false', () => { + const handle1 = manager.create(); + manager.destroy(handle1); + const handle2 = manager.create(); + + // handle1 的 generation 已过期 + expect(manager.isAlive(handle1)).toBe(false); + expect(manager.isAlive(handle2)).toBe(true); + }); + }); + + describe('isEnabled/setEnabled', () => { + it('新创建的句柄默认启用', () => { + const handle = manager.create(); + expect(manager.isEnabled(handle)).toBe(true); + }); + + it('应该能够禁用句柄', () => { + const handle = manager.create(); + manager.setEnabled(handle, false); + expect(manager.isEnabled(handle)).toBe(false); + }); + + it('应该能够重新启用句柄', () => { + const handle = manager.create(); + manager.setEnabled(handle, false); + manager.setEnabled(handle, true); + expect(manager.isEnabled(handle)).toBe(true); + }); + + it('对已销毁句柄设置启用状态不应该报错', () => { + const handle = manager.create(); + manager.destroy(handle); + + expect(() => { + manager.setEnabled(handle, true); + }).not.toThrow(); + }); + + it('已销毁句柄应该返回未启用', () => { + const handle = manager.create(); + manager.destroy(handle); + expect(manager.isEnabled(handle)).toBe(false); + }); + }); + + describe('aliveCount', () => { + it('初始时应该为 0', () => { + expect(manager.aliveCount).toBe(0); + }); + + it('创建句柄后应该增加', () => { + manager.create(); + expect(manager.aliveCount).toBe(1); + + manager.create(); + expect(manager.aliveCount).toBe(2); + }); + + it('销毁句柄后应该减少', () => { + const handle1 = manager.create(); + const handle2 = manager.create(); + expect(manager.aliveCount).toBe(2); + + manager.destroy(handle1); + expect(manager.aliveCount).toBe(1); + + manager.destroy(handle2); + expect(manager.aliveCount).toBe(0); + }); + }); + + describe('reset', () => { + it('应该重置所有状态', () => { + const handle1 = manager.create(); + const handle2 = manager.create(); + manager.destroy(handle1); + + manager.reset(); + + expect(manager.aliveCount).toBe(0); + expect(manager.isAlive(handle2)).toBe(false); + }); + + it('重置后应该能重新创建句柄', () => { + manager.create(); + manager.create(); + manager.reset(); + + const newHandle = manager.create(); + expect(isValidHandle(newHandle)).toBe(true); + expect(manager.isAlive(newHandle)).toBe(true); + }); + }); + + describe('大规模测试', () => { + it('应该能处理大量句柄', () => { + const handles: EntityHandle[] = []; + const count = 10000; + + // 创建大量句柄 + for (let i = 0; i < count; i++) { + handles.push(manager.create()); + } + + expect(manager.aliveCount).toBe(count); + + // 验证所有句柄都是存活的 + for (const handle of handles) { + expect(manager.isAlive(handle)).toBe(true); + } + + // 销毁一半 + for (let i = 0; i < count / 2; i++) { + manager.destroy(handles[i]!); + } + + expect(manager.aliveCount).toBe(count / 2); + }); + + it('应该正确复用已销毁的 index', () => { + // 创建并销毁 + const handle1 = manager.create(); + manager.destroy(handle1); + + // 再次创建 + const handle2 = manager.create(); + + // 应该复用 index + expect(indexOf(handle2)).toBe(indexOf(handle1)); + // 但 generation 增加 + expect(genOf(handle2)).toBe(genOf(handle1) + 1); + }); + }); +}); diff --git a/packages/core/tests/ECS/Core/EpochManager.test.ts b/packages/core/tests/ECS/Core/EpochManager.test.ts new file mode 100644 index 00000000..08e536ab --- /dev/null +++ b/packages/core/tests/ECS/Core/EpochManager.test.ts @@ -0,0 +1,72 @@ +import { EpochManager } from '../../../src/ECS/Core/EpochManager'; + +describe('EpochManager', () => { + let epochManager: EpochManager; + + beforeEach(() => { + epochManager = new EpochManager(); + }); + + describe('初始状态', () => { + it('初始 epoch 应该是 1', () => { + expect(epochManager.current).toBe(1); + }); + }); + + describe('increment', () => { + it('应该递增 epoch', () => { + const initial = epochManager.current; + epochManager.increment(); + expect(epochManager.current).toBe(initial + 1); + }); + + it('应该正确递增多次', () => { + const initial = epochManager.current; + epochManager.increment(); + epochManager.increment(); + epochManager.increment(); + expect(epochManager.current).toBe(initial + 3); + }); + }); + + describe('reset', () => { + it('应该重置 epoch 到 1', () => { + epochManager.increment(); + epochManager.increment(); + expect(epochManager.current).toBeGreaterThan(1); + + epochManager.reset(); + expect(epochManager.current).toBe(1); + }); + }); + + describe('current getter', () => { + it('应该返回当前 epoch', () => { + expect(epochManager.current).toBe(1); + epochManager.increment(); + expect(epochManager.current).toBe(2); + }); + }); + + describe('使用场景', () => { + it('可以用于追踪帧数', () => { + // 模拟 10 帧 + for (let i = 0; i < 10; i++) { + epochManager.increment(); + } + expect(epochManager.current).toBe(11); // 初始 1 + 10 帧 + }); + + it('可以用于变更检测', () => { + // 保存检查点 + const checkpoint = epochManager.current; + + // 模拟几帧 + epochManager.increment(); + epochManager.increment(); + + // 当前 epoch 应该大于检查点 + expect(epochManager.current).toBeGreaterThan(checkpoint); + }); + }); +}); diff --git a/packages/core/tests/ECS/Core/SystemScheduler.test.ts b/packages/core/tests/ECS/Core/SystemScheduler.test.ts new file mode 100644 index 00000000..057d3b60 --- /dev/null +++ b/packages/core/tests/ECS/Core/SystemScheduler.test.ts @@ -0,0 +1,555 @@ +import { + SystemScheduler, + CycleDependencyError, + DEFAULT_STAGE_ORDER, + SystemStage +} from '../../../src/ECS/Core/SystemScheduler'; +import { SystemDependencyGraph } from '../../../src/ECS/Core/SystemDependencyGraph'; +import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; +import { Scene } from '../../../src/ECS/Scene'; +import { Matcher } from '../../../src/ECS/Utils/Matcher'; +import { Entity } from '../../../src/ECS/Entity'; +import { ECSSystem, Stage, Before, After, InSet } from '../../../src/ECS/Decorators'; + +// 测试系统 +@ECSSystem('TestSystemA') +class TestSystemA extends EntitySystem { + public executionOrder: number = 0; + public static executionCounter = 0; + + constructor() { + super(Matcher.nothing()); + } + + protected override process(entities: readonly Entity[]): void { + this.executionOrder = ++TestSystemA.executionCounter; + } +} + +@ECSSystem('TestSystemB') +class TestSystemB extends EntitySystem { + public executionOrder: number = 0; + + constructor() { + super(Matcher.nothing()); + } + + protected override process(entities: readonly Entity[]): void { + this.executionOrder = ++TestSystemA.executionCounter; + } +} + +@ECSSystem('TestSystemC') +class TestSystemC extends EntitySystem { + public executionOrder: number = 0; + + constructor() { + super(Matcher.nothing()); + } + + protected override process(entities: readonly Entity[]): void { + this.executionOrder = ++TestSystemA.executionCounter; + } +} + +describe('SystemDependencyGraph', () => { + let graph: SystemDependencyGraph; + + beforeEach(() => { + graph = new SystemDependencyGraph(); + }); + + describe('addSystemNode', () => { + it('应该添加节点', () => { + graph.addSystemNode('SystemA'); + expect(graph.size).toBe(1); + }); + + it('应该支持添加多个节点', () => { + graph.addSystemNode('SystemA'); + graph.addSystemNode('SystemB'); + expect(graph.size).toBe(2); + }); + }); + + describe('addSetNode', () => { + it('应该添加虚拟集合节点', () => { + graph.addSetNode('CoreSystems'); + expect(graph.size).toBe(1); + }); + }); + + describe('addEdge', () => { + it('应该添加边', () => { + graph.addSystemNode('SystemA'); + graph.addSystemNode('SystemB'); + graph.addEdge('SystemA', 'SystemB'); + + // 拓扑排序后 A 应该在 B 之前 + const sorted = graph.topologicalSort(); + expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB')); + }); + + it('应该忽略自引用边', () => { + graph.addSystemNode('SystemA'); + graph.addEdge('SystemA', 'SystemA'); + + // 不应该产生循环 + const sorted = graph.topologicalSort(); + expect(sorted).toContain('SystemA'); + }); + }); + + describe('topologicalSort', () => { + it('应该返回正确的拓扑顺序', () => { + graph.addSystemNode('SystemA'); + graph.addSystemNode('SystemB'); + graph.addSystemNode('SystemC'); + + // A -> B -> C + graph.addEdge('SystemA', 'SystemB'); + graph.addEdge('SystemB', 'SystemC'); + + const sorted = graph.topologicalSort(); + + expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB')); + expect(sorted.indexOf('SystemB')).toBeLessThan(sorted.indexOf('SystemC')); + }); + + it('应该检测循环依赖', () => { + graph.addSystemNode('SystemA'); + graph.addSystemNode('SystemB'); + + // A -> B -> A (循环) + graph.addEdge('SystemA', 'SystemB'); + graph.addEdge('SystemB', 'SystemA'); + + expect(() => { + graph.topologicalSort(); + }).toThrow(CycleDependencyError); + }); + + it('应该检测多节点循环依赖', () => { + graph.addSystemNode('SystemA'); + graph.addSystemNode('SystemB'); + graph.addSystemNode('SystemC'); + + // A -> B -> C -> A (循环) + graph.addEdge('SystemA', 'SystemB'); + graph.addEdge('SystemB', 'SystemC'); + graph.addEdge('SystemC', 'SystemA'); + + expect(() => { + graph.topologicalSort(); + }).toThrow(CycleDependencyError); + }); + + it('应该处理没有依赖的系统', () => { + graph.addSystemNode('SystemA'); + graph.addSystemNode('SystemB'); + + // 没有边,两个独立系统 + const sorted = graph.topologicalSort(); + + expect(sorted).toHaveLength(2); + expect(sorted).toContain('SystemA'); + expect(sorted).toContain('SystemB'); + }); + + it('不应该包含虚拟节点在结果中', () => { + graph.addSystemNode('SystemA'); + graph.addSetNode('CoreSystems'); + + const sorted = graph.topologicalSort(); + + expect(sorted).toContain('SystemA'); + expect(sorted).not.toContain('set:CoreSystems'); + }); + }); + + describe('buildFromSystems', () => { + it('应该从系统依赖信息构建图', () => { + graph.buildFromSystems([ + { name: 'SystemA', before: ['SystemB'], after: [], sets: [] }, + { name: 'SystemB', before: [], after: [], sets: [] } + ]); + + const sorted = graph.topologicalSort(); + expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB')); + }); + + it('应该处理 after 依赖', () => { + graph.buildFromSystems([ + { name: 'SystemA', before: [], after: [], sets: [] }, + { name: 'SystemB', before: [], after: ['SystemA'], sets: [] } + ]); + + const sorted = graph.topologicalSort(); + expect(sorted.indexOf('SystemA')).toBeLessThan(sorted.indexOf('SystemB')); + }); + + it('应该处理 set 依赖', () => { + graph.buildFromSystems([ + { name: 'SystemA', before: [], after: [], sets: ['CoreSystems'] }, + { name: 'SystemB', before: [], after: [], sets: ['CoreSystems'] } + ]); + + const sorted = graph.topologicalSort(); + expect(sorted).toHaveLength(2); + expect(sorted).toContain('SystemA'); + expect(sorted).toContain('SystemB'); + }); + }); + + describe('clear', () => { + it('应该清除所有节点和边', () => { + graph.addSystemNode('SystemA'); + graph.addSystemNode('SystemB'); + graph.addEdge('SystemA', 'SystemB'); + + graph.clear(); + + expect(graph.size).toBe(0); + }); + }); +}); + +describe('SystemScheduler', () => { + let scheduler: SystemScheduler; + + beforeEach(() => { + scheduler = new SystemScheduler(); + TestSystemA.executionCounter = 0; + }); + + describe('DEFAULT_STAGE_ORDER', () => { + it('应该包含所有阶段', () => { + expect(DEFAULT_STAGE_ORDER).toContain('startup'); + expect(DEFAULT_STAGE_ORDER).toContain('preUpdate'); + expect(DEFAULT_STAGE_ORDER).toContain('update'); + expect(DEFAULT_STAGE_ORDER).toContain('postUpdate'); + expect(DEFAULT_STAGE_ORDER).toContain('cleanup'); + }); + + it('阶段顺序应该正确', () => { + expect(DEFAULT_STAGE_ORDER.indexOf('startup')).toBeLessThan( + DEFAULT_STAGE_ORDER.indexOf('preUpdate') + ); + expect(DEFAULT_STAGE_ORDER.indexOf('preUpdate')).toBeLessThan( + DEFAULT_STAGE_ORDER.indexOf('update') + ); + expect(DEFAULT_STAGE_ORDER.indexOf('update')).toBeLessThan( + DEFAULT_STAGE_ORDER.indexOf('postUpdate') + ); + expect(DEFAULT_STAGE_ORDER.indexOf('postUpdate')).toBeLessThan( + DEFAULT_STAGE_ORDER.indexOf('cleanup') + ); + }); + }); + + describe('getSortedSystems', () => { + it('应该返回排序后的系统', () => { + const systemA = new TestSystemA(); + const systemB = new TestSystemB(); + + const systems = scheduler.getSortedSystems([systemA, systemB], 'update'); + expect(systems.length).toBeGreaterThanOrEqual(2); + }); + + it('应该按 updateOrder 排序', () => { + const systemA = new TestSystemA(); + const systemB = new TestSystemB(); + systemA.updateOrder = 10; + systemB.updateOrder = 5; + + const systems = scheduler.getSortedSystems([systemA, systemB], 'update'); + + // B 的 updateOrder 更小,应该在 A 之前 + expect(systems.indexOf(systemB)).toBeLessThan(systems.indexOf(systemA)); + }); + + it('应该按依赖关系排序', () => { + // 创建带有依赖关系的系统 + @ECSSystem('DepSystemA') + @Stage('update') + class DepSystemA extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + @ECSSystem('DepSystemB') + @Stage('update') + @After('DepSystemA') + class DepSystemB extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + const systemA = new DepSystemA(); + const systemB = new DepSystemB(); + + const systems = scheduler.getSortedSystems([systemA, systemB], 'update'); + const indexA = systems.indexOf(systemA); + const indexB = systems.indexOf(systemB); + + expect(indexA).toBeLessThan(indexB); + }); + + it('应该返回指定阶段的系统', () => { + @ECSSystem('PreUpdateSystem') + @Stage('preUpdate') + class PreUpdateSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + @ECSSystem('UpdateSystem') + @Stage('update') + class UpdateSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + const preSystem = new PreUpdateSystem(); + const updateSystem = new UpdateSystem(); + + const preSystems = scheduler.getSortedSystems([preSystem, updateSystem], 'preUpdate'); + const updateSystems = scheduler.getSortedSystems([preSystem, updateSystem], 'update'); + + expect(preSystems).toContain(preSystem); + expect(preSystems).not.toContain(updateSystem); + + expect(updateSystems).toContain(updateSystem); + expect(updateSystems).not.toContain(preSystem); + }); + }); + + describe('getAllSortedSystems', () => { + it('应该返回所有阶段的系统', () => { + @ECSSystem('AllPreSystem') + @Stage('preUpdate') + class AllPreSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + @ECSSystem('AllUpdateSystem') + @Stage('update') + class AllUpdateSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + const preSystem = new AllPreSystem(); + const updateSystem = new AllUpdateSystem(); + + const allSystems = scheduler.getAllSortedSystems([preSystem, updateSystem]); + + expect(allSystems).toContain(preSystem); + expect(allSystems).toContain(updateSystem); + // preUpdate 阶段在 update 之前 + expect(allSystems.indexOf(preSystem)).toBeLessThan(allSystems.indexOf(updateSystem)); + }); + }); + + describe('markDirty', () => { + it('调用 markDirty 后应该重新排序', () => { + const systemA = new TestSystemA(); + const systemB = new TestSystemB(); + + // 第一次排序 + scheduler.getSortedSystems([systemA, systemB], 'update'); + + // 标记脏 + scheduler.markDirty(); + + // 应该重新排序而不出错 + const systems = scheduler.getSortedSystems([systemA, systemB], 'update'); + expect(systems).toHaveLength(2); + }); + }); + + describe('setUseDependencySort', () => { + it('禁用依赖排序后应该只使用 updateOrder', () => { + @ECSSystem('DisabledDepA') + @Stage('update') + class DisabledDepA extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + @ECSSystem('DisabledDepB') + @Stage('update') + @Before('DisabledDepA') + class DisabledDepB extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + const systemA = new DisabledDepA(); + const systemB = new DisabledDepB(); + systemA.updateOrder = 1; + systemB.updateOrder = 2; + + scheduler.setUseDependencySort(false); + + const systems = scheduler.getSortedSystems([systemA, systemB], 'update'); + // 禁用依赖排序后,按 updateOrder 排序 + expect(systems.indexOf(systemA)).toBeLessThan(systems.indexOf(systemB)); + }); + }); +}); + +describe('调度装饰器', () => { + describe('@Stage', () => { + it('应该设置系统的执行阶段', () => { + @ECSSystem('StageTestSystem') + @Stage('postUpdate') + class StageTestSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + const system = new StageTestSystem(); + expect(system.getStage()).toBe('postUpdate'); + }); + }); + + describe('@Before', () => { + it('应该设置系统的前置依赖', () => { + @ECSSystem('BeforeTestSystem') + @Before('OtherSystem') + class BeforeTestSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + const system = new BeforeTestSystem(); + expect(system.getBefore()).toContain('OtherSystem'); + }); + + it('应该支持多个前置依赖', () => { + @ECSSystem('MultiBeforeSystem') + @Before('SystemA', 'SystemB') + class MultiBeforeSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + const system = new MultiBeforeSystem(); + expect(system.getBefore()).toContain('SystemA'); + expect(system.getBefore()).toContain('SystemB'); + }); + }); + + describe('@After', () => { + it('应该设置系统的后置依赖', () => { + @ECSSystem('AfterTestSystem') + @After('OtherSystem') + class AfterTestSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + const system = new AfterTestSystem(); + expect(system.getAfter()).toContain('OtherSystem'); + }); + }); + + describe('@InSet', () => { + it('应该设置系统所属的集合', () => { + @ECSSystem('InSetTestSystem') + @InSet('CoreSystems') + class InSetTestSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + const system = new InSetTestSystem(); + expect(system.getSets()).toContain('CoreSystems'); + }); + }); + + describe('组合使用', () => { + it('应该支持组合多个装饰器', () => { + @ECSSystem('CombinedSystem') + @Stage('update') + @After('InputSystem') + @Before('RenderSystem') + @InSet('CoreSystems') + class CombinedSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + } + + const system = new CombinedSystem(); + expect(system.getStage()).toBe('update'); + expect(system.getAfter()).toContain('InputSystem'); + expect(system.getBefore()).toContain('RenderSystem'); + expect(system.getSets()).toContain('CoreSystems'); + }); + }); +}); + +describe('Fluent API 调度配置', () => { + it('应该支持 stage() 方法', () => { + @ECSSystem('FluentStageSystem') + class FluentStageSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + this.stage('postUpdate'); + } + } + + const system = new FluentStageSystem(); + expect(system.getStage()).toBe('postUpdate'); + }); + + it('应该支持链式调用', () => { + @ECSSystem('FluentChainSystem') + class FluentChainSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + this.stage('update') + .after('SystemA') + .before('SystemB') + .inSet('CoreSystems'); + } + } + + const system = new FluentChainSystem(); + expect(system.getStage()).toBe('update'); + expect(system.getAfter()).toContain('SystemA'); + expect(system.getBefore()).toContain('SystemB'); + expect(system.getSets()).toContain('CoreSystems'); + }); +}); + +describe('CycleDependencyError', () => { + it('应该包含循环节点信息', () => { + const error = new CycleDependencyError(['SystemA', 'SystemB', 'SystemA']); + expect(error.message).toContain('SystemA'); + expect(error.message).toContain('SystemB'); + expect(error.involvedNodes).toEqual(['SystemA', 'SystemB', 'SystemA']); + }); + + it('应该是 Error 的实例', () => { + const error = new CycleDependencyError(['SystemA']); + expect(error).toBeInstanceOf(Error); + expect(error.name).toBe('CycleDependencyError'); + }); +}); diff --git a/packages/core/tests/ECS/Systems/EntitySystemChangeDetection.test.ts b/packages/core/tests/ECS/Systems/EntitySystemChangeDetection.test.ts new file mode 100644 index 00000000..5326fcfc --- /dev/null +++ b/packages/core/tests/ECS/Systems/EntitySystemChangeDetection.test.ts @@ -0,0 +1,463 @@ +import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { Scene } from '../../../src/ECS/Scene'; +import { Matcher } from '../../../src/ECS/Utils/Matcher'; +import { ECSComponent, ECSSystem } from '../../../src/ECS/Decorators'; + +// 测试组件 +@ECSComponent('ChangeDetect_Position') +class PositionComponent extends Component { + private _x: number; + private _y: number; + + constructor(...args: unknown[]) { + super(); + const [x = 0, y = 0] = args as [number?, number?]; + this._x = x; + this._y = y; + } + + public get x(): number { + return this._x; + } + + public set x(value: number) { + this._x = value; + // 实际使用中需要通过 entity.scene.epochManager.current 获取 + // 这里简化测试,手动调用 markDirty + } + + public get y(): number { + return this._y; + } + + public set y(value: number) { + this._y = value; + } + + public setPosition(x: number, y: number, epoch: number): void { + this._x = x; + this._y = y; + this.markDirty(epoch); + } +} + +@ECSComponent('ChangeDetect_Velocity') +class VelocityComponent extends Component { + private _vx: number; + private _vy: number; + + constructor(...args: unknown[]) { + super(); + const [vx = 0, vy = 0] = args as [number?, number?]; + this._vx = vx; + this._vy = vy; + } + + public get vx(): number { + return this._vx; + } + + public get vy(): number { + return this._vy; + } + + public setVelocity(vx: number, vy: number, epoch: number): void { + this._vx = vx; + this._vy = vy; + this.markDirty(epoch); + } +} + +@ECSComponent('ChangeDetect_Health') +class HealthComponent extends Component { + public current: number; + public max: number; + + constructor(...args: unknown[]) { + super(); + const [current = 100, max = 100] = args as [number?, number?]; + this.current = current; + this.max = max; + } +} + +// 测试系统 - 暴露 protected 方法供测试 +@ECSSystem('ChangeDetectionTestSystem') +class ChangeDetectionTestSystem extends EntitySystem { + public processedEntities: Entity[] = []; + public changedEntities: Entity[] = []; + + constructor() { + super(Matcher.all(PositionComponent, VelocityComponent)); + } + + protected override process(entities: readonly Entity[]): void { + this.processedEntities = [...entities]; + } + + // 暴露 protected 方法供测试 + public testForEachChanged( + entities: readonly Entity[], + componentTypes: any[], + processor: (entity: Entity, index: number) => void, + sinceEpoch?: number + ): void { + this.forEachChanged(entities, componentTypes, processor, sinceEpoch); + } + + public testFilterChanged( + entities: readonly Entity[], + componentTypes: any[], + sinceEpoch?: number + ): Entity[] { + return this.filterChanged(entities, componentTypes, sinceEpoch); + } + + public testHasChanged( + entity: Entity, + componentTypes: any[], + sinceEpoch?: number + ): boolean { + return this.hasChanged(entity, componentTypes, sinceEpoch); + } + + public testSaveEpoch(): void { + this.saveEpoch(); + } + + public testGetLastProcessEpoch(): number { + return this.lastProcessEpoch; + } + + public testGetCurrentEpoch(): number { + return this.currentEpoch; + } +} + +describe('EntitySystem 变更检测', () => { + let scene: Scene; + let system: ChangeDetectionTestSystem; + let entity1: Entity; + let entity2: Entity; + let entity3: Entity; + + beforeEach(() => { + scene = new Scene(); + system = new ChangeDetectionTestSystem(); + scene.addSystem(system); + + // 创建测试实体 + entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + entity1.addComponent(new VelocityComponent(1, 2)); + + entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + entity2.addComponent(new VelocityComponent(3, 4)); + + entity3 = scene.createEntity('entity3'); + entity3.addComponent(new PositionComponent(50, 60)); + entity3.addComponent(new VelocityComponent(5, 6)); + }); + + afterEach(() => { + scene.removeSystem(system); + scene.end(); + }); + + describe('lastProcessEpoch', () => { + it('初始值应该是 0', () => { + expect(system.testGetLastProcessEpoch()).toBe(0); + }); + }); + + describe('currentEpoch', () => { + it('应该返回场景的当前 epoch', () => { + expect(system.testGetCurrentEpoch()).toBe(scene.epochManager.current); + }); + }); + + describe('saveEpoch', () => { + it('应该保存当前 epoch', () => { + const currentEpoch = scene.epochManager.current; + system.testSaveEpoch(); + expect(system.testGetLastProcessEpoch()).toBe(currentEpoch); + }); + + it('递增 epoch 后 saveEpoch 应该保存新值', () => { + scene.epochManager.increment(); + scene.epochManager.increment(); + const currentEpoch = scene.epochManager.current; + + system.testSaveEpoch(); + expect(system.testGetLastProcessEpoch()).toBe(currentEpoch); + }); + }); + + describe('hasChanged', () => { + it('新组件应该被检测为已变更', () => { + // 组件的 lastWriteEpoch 默认是 0,如果检查点也是 0,则不算变更 + // 需要先保存 epoch,然后修改组件 + system.testSaveEpoch(); + scene.epochManager.increment(); + + const pos = entity1.getComponent(PositionComponent)!; + pos.setPosition(100, 200, scene.epochManager.current); + + expect(system.testHasChanged(entity1, [PositionComponent])).toBe(true); + }); + + it('未修改的组件应该不被检测为变更', () => { + system.testSaveEpoch(); + scene.epochManager.increment(); + + // entity2 的组件未被修改 + expect(system.testHasChanged(entity2, [PositionComponent])).toBe(false); + }); + + it('应该检查多个组件类型', () => { + system.testSaveEpoch(); + scene.epochManager.increment(); + + // 只修改 Velocity + const vel = entity1.getComponent(VelocityComponent)!; + vel.setVelocity(10, 20, scene.epochManager.current); + + // 检查 Position 和 Velocity,应该检测到变更 + expect(system.testHasChanged(entity1, [PositionComponent, VelocityComponent])).toBe(true); + }); + + it('指定 sinceEpoch 参数应该使用该值作为检查点', () => { + const pos = entity1.getComponent(PositionComponent)!; + + // 在 epoch 5 修改组件 + scene.epochManager.increment(); // 2 + scene.epochManager.increment(); // 3 + scene.epochManager.increment(); // 4 + scene.epochManager.increment(); // 5 + pos.setPosition(100, 200, 5); + + // 检查 epoch 4 之后的变更 - 应该检测到 + expect(system.testHasChanged(entity1, [PositionComponent], 4)).toBe(true); + + // 检查 epoch 5 之后的变更 - 不应该检测到 + expect(system.testHasChanged(entity1, [PositionComponent], 5)).toBe(false); + }); + }); + + describe('filterChanged', () => { + it('应该返回有变更的实体', () => { + system.testSaveEpoch(); + scene.epochManager.increment(); + + // 只修改 entity1 + const pos1 = entity1.getComponent(PositionComponent)!; + pos1.setPosition(100, 200, scene.epochManager.current); + + const entities = [entity1, entity2, entity3]; + const changed = system.testFilterChanged(entities, [PositionComponent]); + + expect(changed).toHaveLength(1); + expect(changed[0]).toBe(entity1); + }); + + it('应该返回空数组当没有变更时', () => { + system.testSaveEpoch(); + scene.epochManager.increment(); + + // 没有修改任何组件 + const entities = [entity1, entity2, entity3]; + const changed = system.testFilterChanged(entities, [PositionComponent]); + + expect(changed).toHaveLength(0); + }); + + it('应该返回所有变更的实体', () => { + system.testSaveEpoch(); + scene.epochManager.increment(); + + // 修改 entity1 和 entity3 + const pos1 = entity1.getComponent(PositionComponent)!; + pos1.setPosition(100, 200, scene.epochManager.current); + + const pos3 = entity3.getComponent(PositionComponent)!; + pos3.setPosition(500, 600, scene.epochManager.current); + + const entities = [entity1, entity2, entity3]; + const changed = system.testFilterChanged(entities, [PositionComponent]); + + expect(changed).toHaveLength(2); + expect(changed).toContain(entity1); + expect(changed).toContain(entity3); + }); + }); + + describe('forEachChanged', () => { + it('应该只遍历有变更的实体', () => { + system.testSaveEpoch(); + scene.epochManager.increment(); + + // 只修改 entity2 + const pos2 = entity2.getComponent(PositionComponent)!; + pos2.setPosition(300, 400, scene.epochManager.current); + + const entities = [entity1, entity2, entity3]; + const processed: Entity[] = []; + + system.testForEachChanged(entities, [PositionComponent], (entity) => { + processed.push(entity); + }); + + expect(processed).toHaveLength(1); + expect(processed[0]).toBe(entity2); + }); + + it('应该自动更新 lastProcessEpoch', () => { + system.testSaveEpoch(); + const savedEpoch = system.testGetLastProcessEpoch(); + + scene.epochManager.increment(); + const currentEpoch = scene.epochManager.current; + + const entities = [entity1, entity2]; + system.testForEachChanged(entities, [PositionComponent], () => {}); + + // forEachChanged 应该更新 lastProcessEpoch + expect(system.testGetLastProcessEpoch()).toBe(currentEpoch); + expect(system.testGetLastProcessEpoch()).toBeGreaterThan(savedEpoch); + }); + + it('指定 sinceEpoch 时不应该影响自动更新', () => { + scene.epochManager.increment(); + scene.epochManager.increment(); + + const entities = [entity1]; + system.testForEachChanged(entities, [PositionComponent], () => {}, 0); + + // 应该更新到当前 epoch + expect(system.testGetLastProcessEpoch()).toBe(scene.epochManager.current); + }); + }); + + describe('实际使用场景', () => { + it('应该支持增量更新模式', () => { + // 模拟第一帧 + scene.update(); + + // 保存检查点 + system.testSaveEpoch(); + const checkpoint = system.testGetLastProcessEpoch(); + + // 模拟第二帧 - 修改一个实体 + scene.epochManager.increment(); + const pos1 = entity1.getComponent(PositionComponent)!; + pos1.setPosition(100, 200, scene.epochManager.current); + + // 只处理变更的实体 + const changed = system.testFilterChanged(system.entities, [PositionComponent]); + expect(changed).toHaveLength(1); + expect(changed[0]).toBe(entity1); + + // 更新检查点 + system.testSaveEpoch(); + + // 模拟第三帧 - 没有修改 + scene.epochManager.increment(); + + // 不应该有变更 + const noChanges = system.testFilterChanged(system.entities, [PositionComponent]); + expect(noChanges).toHaveLength(0); + }); + + it('应该正确处理多次变更', () => { + system.testSaveEpoch(); + + // 第一次变更 + scene.epochManager.increment(); + const pos1 = entity1.getComponent(PositionComponent)!; + pos1.setPosition(100, 200, scene.epochManager.current); + + let changed = system.testFilterChanged(system.entities, [PositionComponent]); + expect(changed).toContain(entity1); + + // 更新检查点 + system.testSaveEpoch(); + + // 第二次变更 - 不同实体 + scene.epochManager.increment(); + const pos2 = entity2.getComponent(PositionComponent)!; + pos2.setPosition(300, 400, scene.epochManager.current); + + changed = system.testFilterChanged(system.entities, [PositionComponent]); + expect(changed).not.toContain(entity1); + expect(changed).toContain(entity2); + }); + }); + + describe('边界情况', () => { + it('空实体列表应该正常处理', () => { + const processed: Entity[] = []; + system.testForEachChanged([], [PositionComponent], (entity) => { + processed.push(entity); + }); + expect(processed).toHaveLength(0); + }); + + it('空组件类型列表应该不检测到任何变更', () => { + system.testSaveEpoch(); + scene.epochManager.increment(); + + const pos1 = entity1.getComponent(PositionComponent)!; + pos1.setPosition(100, 200, scene.epochManager.current); + + // 空组件类型列表 + const changed = system.testFilterChanged(system.entities, []); + expect(changed).toHaveLength(0); + }); + + it('实体缺少指定组件时应该跳过', () => { + // 创建一个只有 Position 的实体 + const entityWithoutVelocity = scene.createEntity('noVel'); + entityWithoutVelocity.addComponent(new PositionComponent(70, 80)); + + system.testSaveEpoch(); + scene.epochManager.increment(); + + // 修改该实体的 Position + const pos = entityWithoutVelocity.getComponent(PositionComponent)!; + pos.setPosition(100, 200, scene.epochManager.current); + + // 检查 Velocity 变更 - 该实体没有 Velocity,应该不被检测 + const entities = [entityWithoutVelocity]; + const changed = system.testFilterChanged(entities, [VelocityComponent]); + expect(changed).toHaveLength(0); + }); + }); +}); + +describe('Component.markDirty', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene(); + }); + + afterEach(() => { + scene.end(); + }); + + it('应该更新 lastWriteEpoch', () => { + const entity = scene.createEntity('test'); + const pos = entity.addComponent(new PositionComponent(10, 20)); + + expect(pos.lastWriteEpoch).toBe(0); + + pos.markDirty(5); + expect(pos.lastWriteEpoch).toBe(5); + + pos.markDirty(10); + expect(pos.lastWriteEpoch).toBe(10); + }); +});