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:
295
packages/core/src/ECS/Core/CommandBuffer.ts
Normal file
295
packages/core/src/ECS/Core/CommandBuffer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}: 已销毁`);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统执行错误
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
419
packages/core/tests/ECS/Core/CommandBuffer.test.ts
Normal file
419
packages/core/tests/ECS/Core/CommandBuffer.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user