perf(core): 优化 EntitySystem 迭代性能,添加 CommandBuffer 延迟命令 (#281)

* perf(core): 优化 EntitySystem 迭代性能,添加 CommandBuffer 延迟命令

ReactiveQuery 快照优化:
- 添加快照机制,避免每帧拷贝数组
- 只在实体列表变化时创建新快照
- 静态场景下多个系统共享同一快照

CommandBuffer 延迟命令系统:
- 支持延迟添加/移除组件、销毁实体、设置实体激活状态
- 每个系统拥有独立的 commands 属性
- 命令在帧末统一执行,避免迭代过程中修改实体列表

Scene 更新:
- 在 lateUpdate 后自动刷新所有系统的命令缓冲区

文档:
- 更新系统文档,添加 CommandBuffer 使用说明

* fix(ci): upgrade first-interaction action to v1.3.0

Fix Docker build failure in welcome workflow.

* fix(ci): upgrade pnpm/action-setup to v4 and fix unused import

- Upgrade pnpm/action-setup@v2 to v4 in all workflow files
- Remove unused CommandType import in CommandBuffer.test.ts

* fix(ci): remove duplicate pnpm version specification
This commit is contained in:
YHH
2025-12-05 17:24:33 +08:00
committed by GitHub
parent dd130eacb0
commit 13a149c3a2
16 changed files with 1035 additions and 33 deletions
+1 -3
View File
@@ -31,9 +31,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
+1 -3
View File
@@ -15,9 +15,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
+1 -3
View File
@@ -18,9 +18,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
+1 -3
View File
@@ -30,9 +30,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
+1 -3
View File
@@ -34,9 +34,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
+1 -3
View File
@@ -46,9 +46,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
+1 -3
View File
@@ -23,9 +23,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Welcome new contributors - name: Welcome new contributors
uses: actions/first-interaction@v1 uses: actions/first-interaction@v1.3.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: | issue-message: |
+104
View File
@@ -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. 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 ## System Properties and Methods
### Important Properties ### Important Properties
+104
View File
@@ -334,6 +334,110 @@ class DamageSystem extends EntitySystem {
框架会在每次 `process`/`lateProcess` 调用前创建实体列表的快照,确保迭代过程中的组件变化不会导致跳过实体或重复处理。 框架会在每次 `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()` 后命令队列会清空
## 系统属性和方法 ## 系统属性和方法
### 重要属性 ### 重要属性
+295
View File
@@ -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<T extends Component>(entity: Entity, componentType: ComponentType<T>): 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;
}
}
+27 -2
View File
@@ -87,6 +87,14 @@ export class ReactiveQuery {
/** 实体ID集合,用于快速查找 */ /** 实体ID集合,用于快速查找 */
private _entityIdSet: Set<number> = new Set(); private _entityIdSet: Set<number> = 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; 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[] { 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._entities.push(entity);
this._entityIdSet.add(entity.id); this._entityIdSet.add(entity.id);
this._snapshot = null; // 使快照失效 | Invalidate snapshot
// 通知监听器 // 通知监听器
if (this._config.enableBatchMode) { if (this._config.enableBatchMode) {
@@ -273,6 +295,7 @@ export class ReactiveQuery {
this._entities.splice(index, 1); this._entities.splice(index, 1);
} }
this._entityIdSet.delete(entity.id); this._entityIdSet.delete(entity.id);
this._snapshot = null; // 使快照失效 | Invalidate snapshot
// 通知监听器 // 通知监听器
if (this._config.enableBatchMode) { if (this._config.enableBatchMode) {
@@ -320,6 +343,7 @@ export class ReactiveQuery {
// 清空现有结果 // 清空现有结果
this._entities.length = 0; this._entities.length = 0;
this._entityIdSet.clear(); this._entityIdSet.clear();
this._snapshot = null; // 使快照失效 | Invalidate snapshot
// 筛选匹配的实体 // 筛选匹配的实体
for (const entity of entities) { for (const entity of entities) {
@@ -439,6 +463,7 @@ export class ReactiveQuery {
this.unsubscribeAll(); this.unsubscribeAll();
this._entities.length = 0; this._entities.length = 0;
this._entityIdSet.clear(); this._entityIdSet.clear();
this._snapshot = null;
if (this._config.debug) { if (this._config.debug) {
logger.debug(`ReactiveQuery ${this._id}: 已销毁`); logger.debug(`ReactiveQuery ${this._id}: 已销毁`);
+26
View File
@@ -483,6 +483,10 @@ export class Scene implements IScene {
} finally { } finally {
ProfilerSDK.endSample(lateUpdateHandle); ProfilerSDK.endSample(lateUpdateHandle);
} }
// 执行所有系统的延迟命令
// Flush all systems' deferred commands
this.flushCommandBuffers(systems);
} finally { } finally {
ProfilerSDK.endSample(frameHandle); 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);
}
}
/** /**
* *
* *
+50 -9
View File
@@ -10,6 +10,7 @@ import type { EventListenerConfig, TypeSafeEventSystem, EventHandler } from '../
import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHelpers'; import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHelpers';
import type { IService } from '../../Core/ServiceContainer'; import type { IService } from '../../Core/ServiceContainer';
import { EntityCache } from './EntityCache'; import { EntityCache } from './EntityCache';
import { CommandBuffer } from '../Core/CommandBuffer';
/** /**
* *
@@ -93,6 +94,30 @@ export abstract class EntitySystem implements ISystemBase, IService {
*/ */
private _entityCache: EntityCache; 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) { public set scene(value: Scene | null) {
this._scene = value; this._scene = value;
// 同步更新 CommandBuffer 的场景引用
// Sync CommandBuffer scene reference
this.commands.setScene(value);
} }
/** /**
@@ -599,11 +627,9 @@ export abstract class EntitySystem implements ISystemBase, IService {
try { try {
this.onBegin(); this.onBegin();
// 查询实体并存储到帧缓存中 // 查询实体并存储到帧缓存中
// 响应式查询会自动维护最新的实体列表,updateEntityTracking会在检测到变化时invalidate // ReactiveQuery.getEntities() 返回的是安全快照,只在实体变化时才创建新数组
const queriedEntities = this.queryEntities(); // ReactiveQuery.getEntities() returns a safe snapshot, only creates new array when entities change
// 创建数组副本以防止迭代过程中数组被修改 const entities = this.queryEntities();
// Create a copy to prevent array modification during iteration
const entities = [...queriedEntities];
this._entityCache.setFrame(entities); this._entityCache.setFrame(entities);
entityCount = entities.length; entityCount = entities.length;
@@ -630,10 +656,8 @@ export abstract class EntitySystem implements ISystemBase, IService {
// 在 update 和 lateUpdate 之间可能有新组件被添加(事件驱动设计) // 在 update 和 lateUpdate 之间可能有新组件被添加(事件驱动设计)
// Re-query entities to get the latest list // Re-query entities to get the latest list
// New components may have been added between update and lateUpdate (event-driven design) // New components may have been added between update and lateUpdate (event-driven design)
const queriedEntities = this.queryEntities(); // ReactiveQuery.getEntities() 返回的是安全快照,只在实体变化时才创建新数组
// 创建数组副本以防止迭代过程中数组被修改 const entities = this.queryEntities();
// Create a copy to prevent array modification during iteration
const entities = [...queriedEntities];
this._entityCache.setFrame(entities); this._entityCache.setFrame(entities);
entityCount = entities.length; entityCount = entities.length;
this.lateProcess(entities); 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._entityCache.clearAll();
this._entityIdMap = null; this._entityIdMap = null;
// 清理命令缓冲区
// Clear command buffer
this.commands.dispose();
// 重置状态 // 重置状态
this._initialized = false; this._initialized = false;
this._scene = null; this._scene = null;
+2
View File
@@ -19,4 +19,6 @@ export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker';
export type { EntityRefRecord } from './Core/ReferenceTracker'; export type { EntityRefRecord } from './Core/ReferenceTracker';
export { ReactiveQuery, ReactiveQueryChangeType } from './Core/ReactiveQuery'; export { ReactiveQuery, ReactiveQueryChangeType } from './Core/ReactiveQuery';
export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig } 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'; export * from './EntityTags';
@@ -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);
// 第一次 updatesystem.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);
});
});
});