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

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;
}
}

View File

@@ -87,6 +87,14 @@ export class ReactiveQuery {
/** 实体ID集合,用于快速查找 */
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;
@@ -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}: 已销毁`);

View File

@@ -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);
}
}
/**
* 处理系统执行错误
*

View File

@@ -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;

View File

@@ -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';