diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7737fa5c..4af38682 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,7 @@ jobs: uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 10 + uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index ceb3238e..b5787475 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -15,9 +15,7 @@ jobs: uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 10 + uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 13fb3bc7..2539822b 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -18,9 +18,7 @@ jobs: fetch-depth: 0 - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 10 + uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8c38bf2e..459b2184 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,9 +30,7 @@ jobs: fetch-depth: 0 - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 10 + uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/release-editor.yml b/.github/workflows/release-editor.yml index fa75b4f9..22f09624 100644 --- a/.github/workflows/release-editor.yml +++ b/.github/workflows/release-editor.yml @@ -34,9 +34,7 @@ jobs: uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 10 + uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ab3cc3c..99f3e633 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,9 +46,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 10 + uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/size-limit.yml b/.github/workflows/size-limit.yml index fcfcb8b7..cdfc279e 100644 --- a/.github/workflows/size-limit.yml +++ b/.github/workflows/size-limit.yml @@ -23,9 +23,7 @@ jobs: uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 10 + uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index 7c4fabf9..27d94b4c 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Welcome new contributors - uses: actions/first-interaction@v1 + uses: actions/first-interaction@v1.3.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} issue-message: | diff --git a/docs/en/guide/system.md b/docs/en/guide/system.md index 9aa05e61..a8c56eb4 100644 --- a/docs/en/guide/system.md +++ b/docs/en/guide/system.md @@ -334,6 +334,110 @@ class DamageSystem extends EntitySystem { The framework creates a snapshot of the entity list before each `process`/`lateProcess` call, ensuring that component changes during iteration won't cause entities to be skipped or processed multiple times. +## Command Buffer (CommandBuffer) + +> **v2.2.22+** + +CommandBuffer provides a mechanism for deferred execution of entity operations. When you need to destroy entities or perform other operations that might affect iteration during processing, CommandBuffer allows you to defer these operations to the end of the frame. + +### Basic Usage + +Every EntitySystem has a built-in `commands` property: + +```typescript +@ECSSystem('Damage') +class DamageSystem extends EntitySystem { + constructor() { + super(Matcher.all(Health, DamageReceiver)); + } + + protected process(entities: readonly Entity[]): void { + for (const entity of entities) { + const health = entity.getComponent(Health); + const damage = entity.getComponent(DamageReceiver); + + if (health && damage) { + health.current -= damage.amount; + + // Use command buffer to defer component removal + this.commands.removeComponent(entity, DamageReceiver); + + if (health.current <= 0) { + // Defer adding death marker + this.commands.addComponent(entity, new Dead()); + // Defer entity destruction + this.commands.destroyEntity(entity); + } + } + } + } +} +``` + +### Supported Commands + +| Method | Description | +|--------|-------------| +| `addComponent(entity, component)` | Defer adding component | +| `removeComponent(entity, ComponentType)` | Defer removing component | +| `destroyEntity(entity)` | Defer destroying entity | +| `setEntityActive(entity, active)` | Defer setting entity active state | + +### Execution Timing + +Commands in the buffer are automatically executed after the `lateUpdate` phase of each frame. Execution order matches the order commands were queued. + +``` +Scene Update Flow: +1. onBegin() +2. process() +3. lateProcess() +4. onEnd() +5. flushCommandBuffers() <-- Commands execute here +``` + +### Use Cases + +CommandBuffer is suitable for: + +1. **Destroying entities during iteration**: Avoid modifying collection being traversed +2. **Batch deferred operations**: Merge multiple operations to execute at end of frame +3. **Cross-system coordination**: One system marks, another system responds + +```typescript +// Example: Enemy death system +@ECSSystem('EnemyDeath') +class EnemyDeathSystem extends EntitySystem { + constructor() { + super(Matcher.all(Enemy, Health)); + } + + protected process(entities: readonly Entity[]): void { + for (const entity of entities) { + const health = entity.getComponent(Health); + if (health && health.current <= 0) { + // Play death animation, spawn loot, etc. + this.spawnLoot(entity); + + // Defer destruction, doesn't affect current iteration + this.commands.destroyEntity(entity); + } + } + } + + private spawnLoot(entity: Entity): void { + // Loot spawning logic + } +} +``` + +### Notes + +- Commands skip already destroyed entities (safety check) +- Single command failure doesn't affect other commands +- Commands execute in queue order +- Command queue clears after each `flush()` + ## System Properties and Methods ### Important Properties diff --git a/docs/guide/system.md b/docs/guide/system.md index c5a4faa8..efdae0b2 100644 --- a/docs/guide/system.md +++ b/docs/guide/system.md @@ -334,6 +334,110 @@ class DamageSystem extends EntitySystem { 框架会在每次 `process`/`lateProcess` 调用前创建实体列表的快照,确保迭代过程中的组件变化不会导致跳过实体或重复处理。 +## 命令缓冲区 (CommandBuffer) + +> **v2.2.22+** + +CommandBuffer 提供了一种延迟执行实体操作的机制。当你需要在迭代过程中销毁实体或进行其他可能影响迭代的操作时,使用 CommandBuffer 可以将这些操作推迟到帧末统一执行。 + +### 基本用法 + +每个 EntitySystem 都内置了 `commands` 属性: + +```typescript +@ECSSystem('Damage') +class DamageSystem extends EntitySystem { + constructor() { + super(Matcher.all(Health, DamageReceiver)); + } + + protected process(entities: readonly Entity[]): void { + for (const entity of entities) { + const health = entity.getComponent(Health); + const damage = entity.getComponent(DamageReceiver); + + if (health && damage) { + health.current -= damage.amount; + + // 使用命令缓冲区延迟移除组件 + this.commands.removeComponent(entity, DamageReceiver); + + if (health.current <= 0) { + // 延迟添加死亡标记 + this.commands.addComponent(entity, new Dead()); + // 延迟销毁实体 + this.commands.destroyEntity(entity); + } + } + } + } +} +``` + +### 支持的命令 + +| 方法 | 说明 | +|------|------| +| `addComponent(entity, component)` | 延迟添加组件 | +| `removeComponent(entity, ComponentType)` | 延迟移除组件 | +| `destroyEntity(entity)` | 延迟销毁实体 | +| `setEntityActive(entity, active)` | 延迟设置实体激活状态 | + +### 执行时机 + +命令缓冲区中的命令会在每帧的 `lateUpdate` 阶段之后自动执行。执行顺序与命令入队顺序一致。 + +``` +场景更新流程: +1. onBegin() +2. process() +3. lateProcess() +4. onEnd() +5. flushCommandBuffers() <-- 命令在这里执行 +``` + +### 使用场景 + +CommandBuffer 适用于以下场景: + +1. **在迭代中销毁实体**:避免修改正在遍历的集合 +2. **批量延迟操作**:将多个操作合并到帧末执行 +3. **跨系统协调**:一个系统标记,另一个系统响应 + +```typescript +// 示例:敌人死亡系统 +@ECSSystem('EnemyDeath') +class EnemyDeathSystem extends EntitySystem { + constructor() { + super(Matcher.all(Enemy, Health)); + } + + protected process(entities: readonly Entity[]): void { + for (const entity of entities) { + const health = entity.getComponent(Health); + if (health && health.current <= 0) { + // 播放死亡动画、掉落物品等 + this.spawnLoot(entity); + + // 延迟销毁,不影响当前迭代 + this.commands.destroyEntity(entity); + } + } + } + + private spawnLoot(entity: Entity): void { + // 掉落物品逻辑 + } +} +``` + +### 注意事项 + +- 命令会跳过已销毁的实体(安全检查) +- 单个命令执行失败不会影响其他命令 +- 命令按入队顺序执行 +- 每次 `flush()` 后命令队列会清空 + ## 系统属性和方法 ### 重要属性 diff --git a/packages/core/src/ECS/Core/CommandBuffer.ts b/packages/core/src/ECS/Core/CommandBuffer.ts new file mode 100644 index 00000000..de8bc420 --- /dev/null +++ b/packages/core/src/ECS/Core/CommandBuffer.ts @@ -0,0 +1,295 @@ +import { Entity } from '../Entity'; +import { Component } from '../Component'; +import { ComponentType } from './ComponentStorage'; +import { IScene } from '../IScene'; +import { createLogger } from '../../Utils/Logger'; + +const logger = createLogger('CommandBuffer'); + +/** + * 延迟命令类型 + * Deferred command type + */ +export enum CommandType { + /** 添加组件 | Add component */ + ADD_COMPONENT = 'add_component', + /** 移除组件 | Remove component */ + REMOVE_COMPONENT = 'remove_component', + /** 销毁实体 | Destroy entity */ + DESTROY_ENTITY = 'destroy_entity', + /** 设置实体激活状态 | Set entity active state */ + SET_ENTITY_ACTIVE = 'set_entity_active' +} + +/** + * 延迟命令接口 + * Deferred command interface + */ +export interface DeferredCommand { + /** 命令类型 | Command type */ + type: CommandType; + /** 目标实体 | Target entity */ + entity: Entity; + /** 组件实例(用于 ADD_COMPONENT)| Component instance (for ADD_COMPONENT) */ + component?: Component; + /** 组件类型(用于 REMOVE_COMPONENT)| Component type (for REMOVE_COMPONENT) */ + componentType?: ComponentType; + /** 布尔值(用于启用/激活状态)| Boolean value (for enabled/active state) */ + value?: boolean; +} + +/** + * 命令缓冲区 - 用于延迟执行实体操作 + * Command Buffer - for deferred entity operations + * + * 在系统的 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. + * + * @example + * ```typescript + * class DamageSystem extends EntitySystem { + * protected process(entities: readonly Entity[]): void { + * for (const entity of entities) { + * const health = entity.getComponent(Health); + * if (health.value <= 0) { + * // 延迟到帧末执行,不影响当前迭代 + * // Deferred to end of frame, doesn't affect current iteration + * this.commands.addComponent(entity, new DeadMarker()); + * this.commands.destroyEntity(entity); + * } + * } + * } + * } + * ``` + */ +export class CommandBuffer { + /** 命令队列 | Command queue */ + private _commands: DeferredCommand[] = []; + + /** 关联的场景 | Associated scene */ + private _scene: IScene | null = null; + + /** 是否启用调试日志 | Enable debug logging */ + private _debug: boolean = false; + + /** + * 创建命令缓冲区 + * Create command buffer + * + * @param scene - 关联的场景 | Associated scene + * @param debug - 是否启用调试 | Enable debug + */ + constructor(scene?: IScene, debug: boolean = false) { + this._scene = scene ?? null; + this._debug = debug; + } + + /** + * 设置关联的场景 + * Set associated scene + */ + public setScene(scene: IScene | null): void { + this._scene = scene; + } + + /** + * 获取关联的场景 + * Get associated scene + */ + public get scene(): IScene | null { + return this._scene; + } + + /** + * 获取待执行的命令数量 + * Get pending command count + */ + public get pendingCount(): number { + return this._commands.length; + } + + /** + * 检查是否有待执行的命令 + * Check if there are pending commands + */ + public get hasPending(): boolean { + return this._commands.length > 0; + } + + /** + * 延迟添加组件 + * Deferred add component + * + * @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._debug) { + logger.debug(`CommandBuffer: 延迟添加组件 ${component.constructor.name} 到实体 ${entity.name}`); + } + } + + /** + * 延迟移除组件 + * Deferred remove component + * + * @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._debug) { + logger.debug(`CommandBuffer: 延迟移除组件 ${componentType.name} 从实体 ${entity.name}`); + } + } + + /** + * 延迟销毁实体 + * Deferred destroy entity + * + * @param entity - 要销毁的实体 | Entity to destroy + */ + public destroyEntity(entity: Entity): void { + this._commands.push({ + type: CommandType.DESTROY_ENTITY, + entity + }); + + if (this._debug) { + logger.debug(`CommandBuffer: 延迟销毁实体 ${entity.name}`); + } + } + + /** + * 延迟设置实体激活状态 + * Deferred set entity active state + * + * @param entity - 目标实体 | Target entity + * @param active - 激活状态 | Active state + */ + public setEntityActive(entity: Entity, active: boolean): void { + this._commands.push({ + type: CommandType.SET_ENTITY_ACTIVE, + entity, + value: active + }); + + if (this._debug) { + logger.debug(`CommandBuffer: 延迟设置实体 ${entity.name} 激活状态为 ${active}`); + } + } + + /** + * 执行所有待处理的命令 + * Execute all pending commands + * + * 通常在帧末由 Scene 自动调用。 + * Usually called automatically by Scene at end of frame. + * + * @returns 执行的命令数量 | Number of commands executed + */ + public flush(): number { + if (this._commands.length === 0) { + return 0; + } + + const count = this._commands.length; + + if (this._debug) { + logger.debug(`CommandBuffer: 开始执行 ${count} 个延迟命令`); + } + + // 复制命令列表并清空,防止执行过程中有新命令加入 + // Copy and clear command list to prevent new commands during execution + const commands = this._commands; + this._commands = []; + + for (const cmd of commands) { + this.executeCommand(cmd); + } + + if (this._debug) { + logger.debug(`CommandBuffer: 完成执行 ${count} 个延迟命令`); + } + + return count; + } + + /** + * 执行单个命令 + * Execute single command + */ + private executeCommand(cmd: DeferredCommand): void { + // 检查实体是否仍然有效 + // Check if entity is still valid + if (!cmd.entity.scene) { + if (this._debug) { + logger.debug(`CommandBuffer: 跳过命令,实体 ${cmd.entity.name} 已无效`); + } + return; + } + + try { + switch (cmd.type) { + case CommandType.ADD_COMPONENT: + if (cmd.component) { + cmd.entity.addComponent(cmd.component); + } + break; + + case CommandType.REMOVE_COMPONENT: + if (cmd.componentType) { + cmd.entity.removeComponentByType(cmd.componentType); + } + break; + + case CommandType.DESTROY_ENTITY: + cmd.entity.destroy(); + break; + + case CommandType.SET_ENTITY_ACTIVE: + if (cmd.value !== undefined) { + cmd.entity.active = cmd.value; + } + break; + } + } catch (error) { + logger.error(`CommandBuffer: 执行命令失败`, { command: cmd, error }); + } + } + + /** + * 清空所有待处理的命令(不执行) + * Clear all pending commands (without executing) + */ + public clear(): void { + if (this._debug && this._commands.length > 0) { + logger.debug(`CommandBuffer: 清空 ${this._commands.length} 个未执行的命令`); + } + this._commands.length = 0; + } + + /** + * 销毁命令缓冲区 + * Dispose command buffer + */ + public dispose(): void { + this.clear(); + this._scene = null; + } +} + diff --git a/packages/core/src/ECS/Core/ReactiveQuery.ts b/packages/core/src/ECS/Core/ReactiveQuery.ts index 5c2194ab..e680a9b5 100644 --- a/packages/core/src/ECS/Core/ReactiveQuery.ts +++ b/packages/core/src/ECS/Core/ReactiveQuery.ts @@ -87,6 +87,14 @@ export class ReactiveQuery { /** 实体ID集合,用于快速查找 */ private _entityIdSet: Set = new Set(); + /** + * 实体数组快照 - 用于安全迭代 + * Entity array snapshot - for safe iteration + * 只在实体列表变化时才创建新快照,静态场景下所有系统共享同一快照 + * Only create new snapshot when entity list changes, static scenes share the same snapshot + */ + private _snapshot: readonly Entity[] | null = null; + /** 查询条件 */ private readonly _condition: QueryCondition; @@ -179,10 +187,23 @@ export class ReactiveQuery { } /** - * 获取当前查询结果 + * 获取当前查询结果(返回安全快照) + * Get current query results (returns safe snapshot) + * + * 返回的快照在实体列表变化前保持不变,可安全用于迭代。 + * 静态场景下所有系统共享同一快照,避免每帧创建数组副本。 + * + * The returned snapshot remains unchanged until entity list changes, safe for iteration. + * Static scenes share the same snapshot, avoiding array copy every frame. */ public getEntities(): readonly Entity[] { - return this._entities; + // 如果快照有效,直接返回 | Return snapshot if valid + if (this._snapshot !== null) { + return this._snapshot; + } + // 创建新快照 | Create new snapshot + this._snapshot = [...this._entities]; + return this._snapshot; } /** @@ -236,6 +257,7 @@ export class ReactiveQuery { // 添加到结果集 this._entities.push(entity); this._entityIdSet.add(entity.id); + this._snapshot = null; // 使快照失效 | Invalidate snapshot // 通知监听器 if (this._config.enableBatchMode) { @@ -273,6 +295,7 @@ export class ReactiveQuery { this._entities.splice(index, 1); } this._entityIdSet.delete(entity.id); + this._snapshot = null; // 使快照失效 | Invalidate snapshot // 通知监听器 if (this._config.enableBatchMode) { @@ -320,6 +343,7 @@ export class ReactiveQuery { // 清空现有结果 this._entities.length = 0; this._entityIdSet.clear(); + this._snapshot = null; // 使快照失效 | Invalidate snapshot // 筛选匹配的实体 for (const entity of entities) { @@ -439,6 +463,7 @@ export class ReactiveQuery { this.unsubscribeAll(); this._entities.length = 0; this._entityIdSet.clear(); + this._snapshot = null; if (this._config.debug) { logger.debug(`ReactiveQuery ${this._id}: 已销毁`); diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index 88556d08..cc922c31 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -483,6 +483,10 @@ export class Scene implements IScene { } finally { ProfilerSDK.endSample(lateUpdateHandle); } + + // 执行所有系统的延迟命令 + // Flush all systems' deferred commands + this.flushCommandBuffers(systems); } finally { ProfilerSDK.endSample(frameHandle); // 结束性能采样帧 @@ -490,6 +494,28 @@ export class Scene implements IScene { } } + /** + * 执行所有系统的延迟命令 + * Flush all systems' deferred commands + * + * 在帧末统一执行所有通过 CommandBuffer 提交的延迟操作。 + * Execute all deferred operations submitted via CommandBuffer at end of frame. + */ + private flushCommandBuffers(systems: readonly EntitySystem[]): void { + const flushHandle = ProfilerSDK.beginSample('Scene.flushCommandBuffers', ProfileCategory.ECS); + try { + for (const system of systems) { + try { + system.flushCommands(); + } catch (error) { + this.logger.error(`Error flushing commands for system ${system.systemName}:`, error); + } + } + } finally { + ProfilerSDK.endSample(flushHandle); + } + } + /** * 处理系统执行错误 * diff --git a/packages/core/src/ECS/Systems/EntitySystem.ts b/packages/core/src/ECS/Systems/EntitySystem.ts index 25d75d3d..c57b0758 100644 --- a/packages/core/src/ECS/Systems/EntitySystem.ts +++ b/packages/core/src/ECS/Systems/EntitySystem.ts @@ -10,6 +10,7 @@ import type { EventListenerConfig, TypeSafeEventSystem, EventHandler } from '../ import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHelpers'; import type { IService } from '../../Core/ServiceContainer'; import { EntityCache } from './EntityCache'; +import { CommandBuffer } from '../Core/CommandBuffer'; /** * 事件监听器记录 @@ -93,6 +94,30 @@ export abstract class EntitySystem implements ISystemBase, IService { */ private _entityCache: EntityCache; + /** + * 命令缓冲区 - 用于延迟执行实体操作 + * Command buffer - for deferred entity operations + * + * 在 process() 中使用 commands 可以避免迭代过程中修改实体列表, + * 提高性能并保证迭代安全。命令会在帧末由 Scene 统一执行。 + * + * Using commands in process() avoids modifying entity list during iteration, + * improving performance and ensuring iteration safety. Commands are executed by Scene at end of frame. + * + * @example + * ```typescript + * protected process(entities: readonly Entity[]): void { + * for (const entity of entities) { + * if (shouldDie(entity)) { + * // 延迟执行,不影响当前迭代 + * this.commands.destroyEntity(entity); + * } + * } + * } + * ``` + */ + protected readonly commands: CommandBuffer = new CommandBuffer(); + /** * 获取系统处理的实体列表 */ @@ -191,6 +216,9 @@ export abstract class EntitySystem implements ISystemBase, IService { public set scene(value: Scene | null) { this._scene = value; + // 同步更新 CommandBuffer 的场景引用 + // Sync CommandBuffer scene reference + this.commands.setScene(value); } /** @@ -599,11 +627,9 @@ export abstract class EntitySystem implements ISystemBase, IService { try { this.onBegin(); // 查询实体并存储到帧缓存中 - // 响应式查询会自动维护最新的实体列表,updateEntityTracking会在检测到变化时invalidate - const queriedEntities = this.queryEntities(); - // 创建数组副本以防止迭代过程中数组被修改 - // Create a copy to prevent array modification during iteration - const entities = [...queriedEntities]; + // ReactiveQuery.getEntities() 返回的是安全快照,只在实体变化时才创建新数组 + // ReactiveQuery.getEntities() returns a safe snapshot, only creates new array when entities change + const entities = this.queryEntities(); this._entityCache.setFrame(entities); entityCount = entities.length; @@ -630,10 +656,8 @@ export abstract class EntitySystem implements ISystemBase, IService { // 在 update 和 lateUpdate 之间可能有新组件被添加(事件驱动设计) // Re-query entities to get the latest list // New components may have been added between update and lateUpdate (event-driven design) - const queriedEntities = this.queryEntities(); - // 创建数组副本以防止迭代过程中数组被修改 - // Create a copy to prevent array modification during iteration - const entities = [...queriedEntities]; + // ReactiveQuery.getEntities() 返回的是安全快照,只在实体变化时才创建新数组 + const entities = this.queryEntities(); this._entityCache.setFrame(entities); entityCount = entities.length; this.lateProcess(entities); @@ -645,6 +669,19 @@ export abstract class EntitySystem implements ISystemBase, IService { } } + /** + * 执行命令缓冲区中的所有延迟命令 + * Flush all deferred commands in the command buffer + * + * 由 Scene 在帧末自动调用。 + * Called automatically by Scene at end of frame. + * + * @returns 执行的命令数量 | Number of commands executed + */ + public flushCommands(): number { + return this.commands.flush(); + } + /** * 在系统处理开始前调用 * @@ -937,6 +974,10 @@ export abstract class EntitySystem implements ISystemBase, IService { this._entityCache.clearAll(); this._entityIdMap = null; + // 清理命令缓冲区 + // Clear command buffer + this.commands.dispose(); + // 重置状态 this._initialized = false; this._scene = null; diff --git a/packages/core/src/ECS/index.ts b/packages/core/src/ECS/index.ts index f5f8f31d..2a41ba79 100644 --- a/packages/core/src/ECS/index.ts +++ b/packages/core/src/ECS/index.ts @@ -19,4 +19,6 @@ export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker'; export type { EntityRefRecord } from './Core/ReferenceTracker'; export { ReactiveQuery, ReactiveQueryChangeType } from './Core/ReactiveQuery'; export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig } from './Core/ReactiveQuery'; +export { CommandBuffer, CommandType } from './Core/CommandBuffer'; +export type { DeferredCommand } from './Core/CommandBuffer'; export * from './EntityTags'; diff --git a/packages/core/tests/ECS/Core/CommandBuffer.test.ts b/packages/core/tests/ECS/Core/CommandBuffer.test.ts new file mode 100644 index 00000000..5dd45939 --- /dev/null +++ b/packages/core/tests/ECS/Core/CommandBuffer.test.ts @@ -0,0 +1,419 @@ +import { CommandBuffer } from '../../../src/ECS/Core/CommandBuffer'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { Scene } from '../../../src/ECS/Scene'; +import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; +import { Matcher } from '../../../src/ECS/Utils/Matcher'; + +// 测试组件 +class HealthComponent extends Component { + public value: number = 100; + + constructor(...args: unknown[]) { + super(); + const [value = 100] = args as [number?]; + this.value = value; + } +} + +class MarkerComponent extends Component { + public marked: boolean = true; +} + +class VelocityComponent extends Component { + public vx: number = 0; + public vy: number = 0; +} + +// 测试系统 - 使用 CommandBuffer 延迟执行 +class DamageSystem extends EntitySystem { + public processedEntities: Entity[] = []; + + constructor() { + super(Matcher.all(HealthComponent)); + } + + protected override process(entities: readonly Entity[]): void { + this.processedEntities = []; + for (const entity of entities) { + this.processedEntities.push(entity); + const health = entity.getComponent(HealthComponent); + if (health && health.value <= 0) { + // 使用延迟命令添加标记组件 + this.commands.addComponent(entity, new MarkerComponent()); + } + } + } +} + +describe('CommandBuffer', () => { + let commandBuffer: CommandBuffer; + let scene: Scene; + + beforeEach(() => { + scene = new Scene(); + commandBuffer = new CommandBuffer(scene); + }); + + afterEach(() => { + scene.destroyAllEntities(); + }); + + describe('基础功能 | Basic functionality', () => { + test('创建空的 CommandBuffer | should create empty CommandBuffer', () => { + expect(commandBuffer.pendingCount).toBe(0); + expect(commandBuffer.hasPending).toBe(false); + }); + + test('设置和获取场景 | should set and get scene', () => { + const newScene = new Scene(); + commandBuffer.setScene(newScene); + expect(commandBuffer.scene).toBe(newScene); + newScene.destroyAllEntities(); + }); + + test('构造函数参数 | should accept constructor parameters', () => { + const cb = new CommandBuffer(scene, true); + expect(cb.scene).toBe(scene); + }); + }); + + describe('添加组件命令 | Add component command', () => { + test('延迟添加组件 | should defer adding component', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + const component = new MarkerComponent(); + + commandBuffer.addComponent(entity, component); + + // 命令已入队但未执行 + expect(commandBuffer.pendingCount).toBe(1); + expect(commandBuffer.hasPending).toBe(true); + expect(entity.hasComponent(MarkerComponent)).toBe(false); + + // 执行命令 + const executedCount = commandBuffer.flush(); + + expect(executedCount).toBe(1); + expect(commandBuffer.pendingCount).toBe(0); + expect(entity.hasComponent(MarkerComponent)).toBe(true); + }); + + test('多个添加组件命令 | should handle multiple add component commands', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + + commandBuffer.addComponent(entity, new HealthComponent(50)); + commandBuffer.addComponent(entity, new MarkerComponent()); + commandBuffer.addComponent(entity, new VelocityComponent()); + + expect(commandBuffer.pendingCount).toBe(3); + + commandBuffer.flush(); + + expect(entity.hasComponent(HealthComponent)).toBe(true); + expect(entity.hasComponent(MarkerComponent)).toBe(true); + expect(entity.hasComponent(VelocityComponent)).toBe(true); + }); + }); + + describe('移除组件命令 | Remove component command', () => { + test('延迟移除组件 | should defer removing component', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new HealthComponent(100)); + scene.addEntity(entity); + + expect(entity.hasComponent(HealthComponent)).toBe(true); + + commandBuffer.removeComponent(entity, HealthComponent); + + // 命令已入队但未执行 + expect(commandBuffer.pendingCount).toBe(1); + expect(entity.hasComponent(HealthComponent)).toBe(true); + + // 执行命令 + commandBuffer.flush(); + + expect(entity.hasComponent(HealthComponent)).toBe(false); + }); + + test('移除不存在的组件不报错 | should not throw when removing non-existent component', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + + commandBuffer.removeComponent(entity, HealthComponent); + + expect(() => commandBuffer.flush()).not.toThrow(); + }); + }); + + describe('销毁实体命令 | Destroy entity command', () => { + test('延迟销毁实体 | should defer destroying entity', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + + expect(entity.isDestroyed).toBe(false); + + commandBuffer.destroyEntity(entity); + + // 命令已入队但未执行 + expect(commandBuffer.pendingCount).toBe(1); + expect(entity.isDestroyed).toBe(false); + + // 执行命令 + commandBuffer.flush(); + + expect(entity.isDestroyed).toBe(true); + }); + + test('多个实体销毁命令 | should handle multiple destroy commands', () => { + const entity1 = scene.createEntity('test1'); + const entity2 = scene.createEntity('test2'); + const entity3 = scene.createEntity('test3'); + scene.addEntity(entity1); + scene.addEntity(entity2); + scene.addEntity(entity3); + + commandBuffer.destroyEntity(entity1); + commandBuffer.destroyEntity(entity2); + commandBuffer.destroyEntity(entity3); + + expect(commandBuffer.pendingCount).toBe(3); + + commandBuffer.flush(); + + expect(entity1.isDestroyed).toBe(true); + expect(entity2.isDestroyed).toBe(true); + expect(entity3.isDestroyed).toBe(true); + }); + }); + + describe('设置实体激活状态命令 | Set entity active command', () => { + test('延迟设置实体激活状态 | should defer setting entity active state', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + entity.active = true; + + commandBuffer.setEntityActive(entity, false); + + // 命令已入队但未执行 + expect(entity.active).toBe(true); + + // 执行命令 + commandBuffer.flush(); + + expect(entity.active).toBe(false); + }); + + test('切换激活状态 | should toggle active state', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + entity.active = true; + + commandBuffer.setEntityActive(entity, false); + commandBuffer.flush(); + expect(entity.active).toBe(false); + + commandBuffer.setEntityActive(entity, true); + commandBuffer.flush(); + expect(entity.active).toBe(true); + }); + }); + + describe('命令执行安全性 | Command execution safety', () => { + test('跳过已销毁的实体 | should skip destroyed entities', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + + commandBuffer.addComponent(entity, new MarkerComponent()); + + // 在执行前销毁实体 + entity.destroy(); + + // 执行命令不应报错 + expect(() => commandBuffer.flush()).not.toThrow(); + expect(commandBuffer.pendingCount).toBe(0); + }); + + test('flush 过程中添加新命令 | should handle commands added during flush', () => { + const entity1 = scene.createEntity('test1'); + const entity2 = scene.createEntity('test2'); + scene.addEntity(entity1); + scene.addEntity(entity2); + + // 创建一个特殊的命令缓冲区,在 flush 时会添加新命令 + // 但由于 flush 复制了命令列表,新命令不会在本次 flush 中执行 + commandBuffer.addComponent(entity1, new MarkerComponent()); + + const executedCount = commandBuffer.flush(); + + expect(executedCount).toBe(1); + expect(entity1.hasComponent(MarkerComponent)).toBe(true); + }); + + test('单个命令失败不影响其他命令 | single command failure should not affect others', () => { + const entity1 = scene.createEntity('test1'); + const entity2 = scene.createEntity('test2'); + scene.addEntity(entity1); + scene.addEntity(entity2); + + commandBuffer.addComponent(entity1, new MarkerComponent()); + commandBuffer.addComponent(entity2, new HealthComponent()); + + // 销毁 entity1,使第一个命令失效 + entity1.destroy(); + + // 执行命令,第一个失败,第二个应该成功 + commandBuffer.flush(); + + expect(entity2.hasComponent(HealthComponent)).toBe(true); + }); + }); + + describe('clear 和 dispose | clear and dispose', () => { + test('clear 清空命令但不执行 | should clear commands without executing', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + + commandBuffer.addComponent(entity, new MarkerComponent()); + commandBuffer.destroyEntity(entity); + + expect(commandBuffer.pendingCount).toBe(2); + + commandBuffer.clear(); + + expect(commandBuffer.pendingCount).toBe(0); + expect(entity.hasComponent(MarkerComponent)).toBe(false); + expect(entity.scene).toBe(scene); + }); + + test('dispose 清空命令和场景引用 | should dispose buffer', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + + commandBuffer.addComponent(entity, new MarkerComponent()); + expect(commandBuffer.scene).toBe(scene); + + commandBuffer.dispose(); + + expect(commandBuffer.pendingCount).toBe(0); + expect(commandBuffer.scene).toBeNull(); + }); + }); + + describe('混合命令 | Mixed commands', () => { + test('复杂命令序列 | should handle complex command sequence', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new HealthComponent(100)); + scene.addEntity(entity); + + // 添加一个组件 + commandBuffer.addComponent(entity, new MarkerComponent()); + // 移除原有组件 + commandBuffer.removeComponent(entity, HealthComponent); + // 再添加一个组件 + commandBuffer.addComponent(entity, new VelocityComponent()); + + expect(commandBuffer.pendingCount).toBe(3); + + commandBuffer.flush(); + + expect(entity.hasComponent(MarkerComponent)).toBe(true); + expect(entity.hasComponent(HealthComponent)).toBe(false); + expect(entity.hasComponent(VelocityComponent)).toBe(true); + }); + + test('先添加后销毁 | should handle add then destroy sequence', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + + commandBuffer.addComponent(entity, new MarkerComponent()); + commandBuffer.destroyEntity(entity); + + commandBuffer.flush(); + + // 实体已销毁,组件添加应该已执行(在销毁前) + expect(entity.isDestroyed).toBe(true); + }); + }); + + describe('与 EntitySystem 集成 | Integration with EntitySystem', () => { + test('系统可以使用 commands 属性 | system should have commands property', () => { + const system = new DamageSystem(); + scene.addSystem(system); + + expect(system['commands']).toBeInstanceOf(CommandBuffer); + }); + + test('系统中使用延迟命令 | should use deferred commands in system', () => { + const entity = scene.createEntity('damaged'); + entity.addComponent(new HealthComponent(0)); // 生命值为0 + scene.addEntity(entity); + + const system = new DamageSystem(); + scene.addSystem(system); + + // 第一次 update:system.process 会添加延迟命令 + scene.update(); + + // 检查系统处理了实体 + expect(system.processedEntities.length).toBe(1); + + // 命令应该已经被 flush 执行了(在 Scene.update 的末尾) + expect(entity.hasComponent(MarkerComponent)).toBe(true); + }); + + test('延迟命令不影响当前帧迭代 | deferred commands should not affect current iteration', () => { + // 创建多个实体 + const entities: Entity[] = []; + for (let i = 0; i < 5; i++) { + const entity = scene.createEntity(`entity${i}`); + entity.addComponent(new HealthComponent(i === 2 ? 0 : 100)); // entity2 的生命值为0 + scene.addEntity(entity); + entities.push(entity); + } + + const system = new DamageSystem(); + scene.addSystem(system); + + // update 执行 + scene.update(); + + // 所有5个实体都应该被处理(延迟命令不影响迭代) + expect(system.processedEntities.length).toBe(5); + + // entity2 应该有 MarkerComponent + expect(entities[2].hasComponent(MarkerComponent)).toBe(true); + }); + }); + + describe('边界情况 | Edge cases', () => { + test('空的 flush | should handle empty flush', () => { + const count = commandBuffer.flush(); + expect(count).toBe(0); + }); + + test('多次 flush | should handle multiple flushes', () => { + const entity = scene.createEntity('test'); + scene.addEntity(entity); + + commandBuffer.addComponent(entity, new MarkerComponent()); + expect(commandBuffer.flush()).toBe(1); + expect(commandBuffer.flush()).toBe(0); // 第二次应该是空的 + }); + + test('无场景的 CommandBuffer | should work without scene', () => { + const cb = new CommandBuffer(); + expect(cb.scene).toBeNull(); + + // 仍然可以入队命令 + const entity = scene.createEntity('test'); + scene.addEntity(entity); + cb.addComponent(entity, new MarkerComponent()); + + expect(cb.pendingCount).toBe(1); + cb.flush(); + expect(entity.hasComponent(MarkerComponent)).toBe(true); + }); + }); +});