diff --git a/docs/en/guide/system.md b/docs/en/guide/system.md new file mode 100644 index 00000000..9aa05e61 --- /dev/null +++ b/docs/en/guide/system.md @@ -0,0 +1,820 @@ +# System Architecture + +In ECS architecture, Systems are where business logic is processed. Systems are responsible for performing operations on entities that have specific component combinations, serving as the logic processing units of ECS architecture. + +## Basic Concepts + +Systems are concrete classes that inherit from the `EntitySystem` abstract base class, used for: +- Defining entity processing logic (such as movement, collision detection, rendering, etc.) +- Filtering entities based on component combinations +- Providing lifecycle management and performance monitoring +- Managing entity add/remove events + +## System Types + +The framework provides several different system base classes: + +### EntitySystem - Base System + +The most basic system class, all other systems inherit from it: + +```typescript +import { EntitySystem, ECSSystem, Matcher } from '@esengine/ecs-framework'; + +@ECSSystem('Movement') +class MovementSystem extends EntitySystem { + constructor() { + // Use Matcher to define entity conditions to process + super(Matcher.all(Position, Velocity)); + } + + protected process(entities: readonly Entity[]): void { + for (const entity of entities) { + const position = entity.getComponent(Position); + const velocity = entity.getComponent(Velocity); + + if (position && velocity) { + position.x += velocity.dx * Time.deltaTime; + position.y += velocity.dy * Time.deltaTime; + } + } + } +} +``` + +### ProcessingSystem - Processing System + +Suitable for systems that don't need to process entities individually: + +```typescript +@ECSSystem('Physics') +class PhysicsSystem extends ProcessingSystem { + constructor() { + super(); // No Matcher needed + } + + public processSystem(): void { + // Execute physics world step + this.physicsWorld.step(Time.deltaTime); + } +} +``` + +### PassiveSystem - Passive System + +Passive systems don't actively process, mainly used for listening to entity add and remove events: + +```typescript +@ECSSystem('EntityTracker') +class EntityTrackerSystem extends PassiveSystem { + constructor() { + super(Matcher.all(Health)); + } + + protected onAdded(entity: Entity): void { + console.log(`Health entity added: ${entity.name}`); + } + + protected onRemoved(entity: Entity): void { + console.log(`Health entity removed: ${entity.name}`); + } +} +``` + +### IntervalSystem - Interval System + +Systems that execute at fixed time intervals: + +```typescript +@ECSSystem('AutoSave') +class AutoSaveSystem extends IntervalSystem { + constructor() { + // Execute every 5 seconds + super(5.0, Matcher.all(SaveData)); + } + + protected process(entities: readonly Entity[]): void { + console.log('Executing auto save...'); + // Save game data + this.saveGameData(entities); + } + + private saveGameData(entities: readonly Entity[]): void { + // Save logic + } +} +``` + +### WorkerEntitySystem - Multi-threaded System + +A Web Worker-based multi-threaded processing system, suitable for compute-intensive tasks, capable of fully utilizing multi-core CPU performance. + +Worker systems provide true parallel computing capabilities, support SharedArrayBuffer optimization, and have automatic fallback support. Particularly suitable for physics simulation, particle systems, AI computation, and similar scenarios. + +**For detailed content, please refer to: [Worker System](/guide/worker-system)** + +## Entity Matcher + +Matcher is used to define which entities a system needs to process. It provides flexible condition combinations: + +### Basic Match Conditions + +```typescript +// Must have both Position and Velocity components +const matcher1 = Matcher.all(Position, Velocity); + +// Must have at least one of Health or Shield components +const matcher2 = Matcher.any(Health, Shield); + +// Must not have Dead component +const matcher3 = Matcher.none(Dead); +``` + +### Compound Match Conditions + +```typescript +// Complex combination conditions +const complexMatcher = Matcher.all(Position, Velocity) + .any(Player, Enemy) + .none(Dead, Disabled); + +@ECSSystem('Combat') +class CombatSystem extends EntitySystem { + constructor() { + super(complexMatcher); + } +} +``` + +### Special Match Conditions + +```typescript +// Match by tag +const tagMatcher = Matcher.byTag(1); // Match entities with tag 1 + +// Match by name +const nameMatcher = Matcher.byName("Player"); // Match entities named "Player" + +// Single component match +const componentMatcher = Matcher.byComponent(Health); // Match entities with Health component + +// Match no entities +const nothingMatcher = Matcher.nothing(); // For systems that only need lifecycle callbacks +``` + +### Empty Matcher vs Nothing Matcher + +```typescript +// empty() - Empty condition, matches all entities +const emptyMatcher = Matcher.empty(); + +// nothing() - Matches no entities, for systems that only need lifecycle methods +const nothingMatcher = Matcher.nothing(); + +// Use case: Systems that only need onBegin/onEnd lifecycle +@ECSSystem('FrameTimer') +class FrameTimerSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); // Process no entities + } + + protected onBegin(): void { + // Execute at the start of each frame, e.g., record frame start time + console.log('Frame started'); + } + + protected process(entities: readonly Entity[]): void { + // Never called because there are no matching entities + } + + protected onEnd(): void { + // Execute at the end of each frame + console.log('Frame ended'); + } +} +``` + +> **Tip**: For more details on Matcher and entity queries, please refer to the [Entity Query System](/guide/entity-query) documentation. + +## System Lifecycle + +Systems provide complete lifecycle callbacks: + +```typescript +@ECSSystem('Example') +class ExampleSystem extends EntitySystem { + protected onInitialize(): void { + console.log('System initialized'); + // Called when system is added to scene, for initializing resources + } + + protected onBegin(): void { + // Called before each frame's processing begins + } + + protected process(entities: readonly Entity[]): void { + // Main processing logic + for (const entity of entities) { + // Process each entity + // Safe to add/remove components here without affecting current iteration + } + } + + protected lateProcess(entities: readonly Entity[]): void { + // Post-processing after main process + // Safe to add/remove components here without affecting current iteration + } + + protected onEnd(): void { + // Called after each frame's processing ends + } + + protected onDestroy(): void { + console.log('System destroyed'); + // Called when system is removed from scene, for cleaning up resources + } +} +``` + +## Entity Event Listening + +Systems can listen for entity add and remove events: + +```typescript +@ECSSystem('EnemyManager') +class EnemyManagerSystem extends EntitySystem { + private enemyCount = 0; + + constructor() { + super(Matcher.all(Enemy, Health)); + } + + protected onAdded(entity: Entity): void { + this.enemyCount++; + console.log(`Enemy joined battle, current enemy count: ${this.enemyCount}`); + + // Can set initial state for new enemies here + const health = entity.getComponent(Health); + if (health) { + health.current = health.max; + } + } + + protected onRemoved(entity: Entity): void { + this.enemyCount--; + console.log(`Enemy removed, remaining enemies: ${this.enemyCount}`); + + // Check if all enemies are defeated + if (this.enemyCount === 0) { + this.scene?.eventSystem.emitSync('all_enemies_defeated'); + } + } +} +``` + +### Important: Timing of onAdded/onRemoved Calls + +> **Note**: `onAdded` and `onRemoved` callbacks are called **synchronously**, executing immediately **before** `addComponent`/`removeComponent` returns. + +This means: + +```typescript +// Wrong: Chain assignment executes after onAdded +const comp = entity.addComponent(new ClickComponent()); +comp.element = this._element; // At this point onAdded has already executed! + +// Correct: Pass initial values through constructor +const comp = entity.addComponent(new ClickComponent(this._element)); + +// Or use the createComponent method +const comp = entity.createComponent(ClickComponent, this._element); +``` + +**Why this design?** + +The event-driven design ensures that `onAdded`/`onRemoved` callbacks are not affected by system registration order. When a component is added, all systems listening for that component receive notification immediately, rather than waiting until the next frame. + +**Best Practices:** + +1. Component initial values should be passed through the **constructor** +2. Don't rely on setting properties after `addComponent` returns +3. If you need to access component properties in `onAdded`, ensure those properties are set at construction time + +### Safely Modifying Components in process/lateProcess + +When iterating entities in `process` or `lateProcess`, you can safely add or remove components without affecting the current iteration: + +```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; + + // Safe: removing component won't affect current iteration + entity.removeComponent(damage); + + if (health.current <= 0) { + // Safe: adding component won't affect current iteration + entity.addComponent(new Dead()); + } + } + } + } +} +``` + +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. + +## System Properties and Methods + +### Important Properties + +```typescript +@ECSSystem('Example') +class ExampleSystem extends EntitySystem { + showSystemInfo(): void { + console.log(`System name: ${this.systemName}`); // System name + console.log(`Update order: ${this.updateOrder}`); // Update order + console.log(`Is enabled: ${this.enabled}`); // Enabled state + console.log(`Entity count: ${this.entities.length}`); // Number of matched entities + console.log(`Scene: ${this.scene?.name}`); // Parent scene + } +} +``` + +### Entity Access + +```typescript +protected process(entities: readonly Entity[]): void { + // Method 1: Use entity list from parameter + for (const entity of entities) { + // Process entity + } + + // Method 2: Use this.entities property (same as parameter) + for (const entity of this.entities) { + // Process entity + } +} +``` + +### Controlling System Execution + +```typescript +@ECSSystem('Conditional') +class ConditionalSystem extends EntitySystem { + private shouldProcess = true; + + protected onCheckProcessing(): boolean { + // Return false to skip this processing + return this.shouldProcess && this.entities.length > 0; + } + + public pause(): void { + this.shouldProcess = false; + } + + public resume(): void { + this.shouldProcess = true; + } +} +``` + +## Event System Integration + +Systems can conveniently listen for and send events: + +```typescript +@ECSSystem('GameLogic') +class GameLogicSystem extends EntitySystem { + protected onInitialize(): void { + // Add event listeners (automatically cleaned up when system is destroyed) + this.addEventListener('player_died', this.onPlayerDied.bind(this)); + this.addEventListener('level_complete', this.onLevelComplete.bind(this)); + } + + private onPlayerDied(data: any): void { + console.log('Player died, restarting game'); + // Handle player death logic + } + + private onLevelComplete(data: any): void { + console.log('Level complete, loading next level'); + // Handle level completion logic + } + + protected process(entities: readonly Entity[]): void { + // Send events during processing + for (const entity of entities) { + const health = entity.getComponent(Health); + if (health && health.current <= 0) { + this.scene?.eventSystem.emitSync('entity_died', { entity }); + } + } + } +} +``` + +## Performance Monitoring + +Systems have built-in performance monitoring: + +```typescript +@ECSSystem('Performance') +class PerformanceSystem extends EntitySystem { + protected onEnd(): void { + // Get performance data + const perfData = this.getPerformanceData(); + if (perfData) { + console.log(`Execution time: ${perfData.executionTime.toFixed(2)}ms`); + } + + // Get performance statistics + const stats = this.getPerformanceStats(); + if (stats) { + console.log(`Average execution time: ${stats.averageTime.toFixed(2)}ms`); + } + } + + public resetPerformance(): void { + this.resetPerformanceData(); + } +} +``` + +## System Management + +### Adding Systems to Scene + +The framework provides two ways to add systems: pass an instance or pass a type (automatic dependency injection). + +```typescript +// Add systems in scene subclass +class GameScene extends Scene { + protected initialize(): void { + // Method 1: Pass instance + this.addSystem(new MovementSystem()); + this.addSystem(new RenderSystem()); + + // Method 2: Pass type (automatic dependency injection) + this.addEntityProcessor(PhysicsSystem); + + // Set system update order + const movementSystem = this.getSystem(MovementSystem); + if (movementSystem) { + movementSystem.updateOrder = 1; + } + } +} +``` + +### System Dependency Injection + +Systems implement the `IService` interface and support obtaining other services or systems through dependency injection: + +```typescript +import { ECSSystem, Injectable, Inject } from '@esengine/ecs-framework'; + +@Injectable() +@ECSSystem('Physics') +class PhysicsSystem extends EntitySystem { + constructor( + @Inject(CollisionService) private collision: CollisionService + ) { + super(Matcher.all(Transform, RigidBody)); + } + + protected process(entities: readonly Entity[]): void { + // Use injected service + this.collision.detectCollisions(entities); + } + + // Implement IService interface dispose method + public dispose(): void { + // Clean up resources + } +} + +// Just pass the type when using, framework will auto-inject dependencies +class GameScene extends Scene { + protected initialize(): void { + // Automatic dependency injection + this.addEntityProcessor(PhysicsSystem); + } +} +``` + +Notes: +- Use `@Injectable()` decorator to mark systems that need dependency injection +- Use `@Inject()` decorator in constructor parameters to declare dependencies +- Systems must implement the `dispose()` method (IService interface requirement) +- Use `addEntityProcessor(Type)` instead of `addSystem(new Type())` to enable dependency injection + +### System Update Order + +System execution order is determined by the `updateOrder` property. Lower values execute first: + +```typescript +@ECSSystem('Input') +class InputSystem extends EntitySystem { + constructor() { + super(Matcher.all(InputComponent)); + this.updateOrder = -100; // Input system executes first + } +} + +@ECSSystem('Physics') +class PhysicsSystem extends EntitySystem { + constructor() { + super(Matcher.all(RigidBody)); + this.updateOrder = 0; // Default order + } +} + +@ECSSystem('Render') +class RenderSystem extends EntitySystem { + constructor() { + super(Matcher.all(Sprite, Transform)); + this.updateOrder = 100; // Render system executes last + } +} +``` + +#### Stable Sorting: addOrder + +When multiple systems have the same `updateOrder`, the framework uses `addOrder` (add order) as a secondary sorting criterion to ensure stable and predictable results: + +```typescript +// Both systems have default updateOrder of 0 +@ECSSystem('SystemA') +class SystemA extends EntitySystem { /* ... */ } + +@ECSSystem('SystemB') +class SystemB extends EntitySystem { /* ... */ } + +// Add order determines execution order +scene.addSystem(new SystemA()); // addOrder = 0, executes first +scene.addSystem(new SystemB()); // addOrder = 1, executes second +``` + +> **Note**: `addOrder` is automatically set by the framework when calling `addSystem`, no manual management needed. This ensures systems with the same `updateOrder` execute in their addition order, avoiding random behavior from unstable sorting. + +## Complex System Examples + +### Collision Detection System + +```typescript +@ECSSystem('Collision') +class CollisionSystem extends EntitySystem { + constructor() { + super(Matcher.all(Transform, Collider)); + } + + protected process(entities: readonly Entity[]): void { + // Simple n² collision detection + for (let i = 0; i < entities.length; i++) { + for (let j = i + 1; j < entities.length; j++) { + this.checkCollision(entities[i], entities[j]); + } + } + } + + private checkCollision(entityA: Entity, entityB: Entity): void { + const transformA = entityA.getComponent(Transform); + const transformB = entityB.getComponent(Transform); + const colliderA = entityA.getComponent(Collider); + const colliderB = entityB.getComponent(Collider); + + if (this.isColliding(transformA, colliderA, transformB, colliderB)) { + // Send collision event + this.scene?.eventSystem.emitSync('collision', { + entityA, + entityB + }); + } + } + + private isColliding(transformA: Transform, colliderA: Collider, + transformB: Transform, colliderB: Collider): boolean { + // Collision detection logic + return false; // Simplified example + } +} +``` + +### State Machine System + +```typescript +@ECSSystem('StateMachine') +class StateMachineSystem extends EntitySystem { + constructor() { + super(Matcher.all(StateMachine)); + } + + protected process(entities: readonly Entity[]): void { + for (const entity of entities) { + const stateMachine = entity.getComponent(StateMachine); + if (stateMachine) { + stateMachine.updateTimer(Time.deltaTime); + this.updateState(entity, stateMachine); + } + } + } + + private updateState(entity: Entity, stateMachine: StateMachine): void { + switch (stateMachine.currentState) { + case EntityState.Idle: + this.handleIdleState(entity, stateMachine); + break; + case EntityState.Moving: + this.handleMovingState(entity, stateMachine); + break; + case EntityState.Attacking: + this.handleAttackingState(entity, stateMachine); + break; + } + } + + private handleIdleState(entity: Entity, stateMachine: StateMachine): void { + // Idle state logic + } + + private handleMovingState(entity: Entity, stateMachine: StateMachine): void { + // Moving state logic + } + + private handleAttackingState(entity: Entity, stateMachine: StateMachine): void { + // Attacking state logic + } +} +``` + +## Best Practices + +### 1. Single Responsibility for Systems + +```typescript +// Good system design - single responsibility +@ECSSystem('Movement') +class MovementSystem extends EntitySystem { + constructor() { + super(Matcher.all(Position, Velocity)); + } +} + +@ECSSystem('Rendering') +class RenderingSystem extends EntitySystem { + constructor() { + super(Matcher.all(Sprite, Transform)); + } +} + +// Avoid - too many responsibilities +@ECSSystem('GameSystem') +class GameSystem extends EntitySystem { + // One system handling movement, rendering, sound effects, and more +} +``` + +### 2. Use @ECSSystem Decorator + +`@ECSSystem` is a required decorator for system classes, providing type identification and metadata management. + +#### Why It's Required + +| Feature | Description | +|---------|-------------| +| **Type Identification** | Provides stable system names that remain correct after code obfuscation | +| **Debug Support** | Shows readable system names in performance monitoring, logs, and debug tools | +| **System Management** | Find and manage systems by name | +| **Serialization Support** | Records system configuration during scene serialization | + +#### Basic Syntax + +```typescript +@ECSSystem(systemName: string) +``` + +- `systemName`: The system's name, recommend using descriptive names + +#### Usage Example + +```typescript +// Correct usage +@ECSSystem('Physics') +class PhysicsSystem extends EntitySystem { + // System implementation +} + +// Recommended: Use descriptive names +@ECSSystem('PlayerMovement') +class PlayerMovementSystem extends EntitySystem { + constructor() { + super(Matcher.all(Player, Position, Velocity)); + } +} + +// Wrong - no decorator +class BadSystem extends EntitySystem { + // Systems defined this way may have issues in production: + // 1. Class name changes after code minification, can't identify correctly + // 2. Performance monitoring and debug tools show incorrect names +} +``` + +#### System Name Usage + +```typescript +@ECSSystem('Combat') +class CombatSystem extends EntitySystem { + protected onInitialize(): void { + // Access system name using systemName property + console.log(`System ${this.systemName} initialized`); // Output: System Combat initialized + } +} + +// Find system by name +const combat = scene.getSystemByName('Combat'); + +// Performance monitoring displays system name +const perfData = combatSystem.getPerformanceData(); +console.log(`${combatSystem.systemName} execution time: ${perfData?.executionTime}ms`); +``` + +### 3. Proper Update Order + +```typescript +// Set system update order by logical sequence +@ECSSystem('Input') +class InputSystem extends EntitySystem { + constructor() { + super(); + this.updateOrder = -100; // Process input first + } +} + +@ECSSystem('Logic') +class GameLogicSystem extends EntitySystem { + constructor() { + super(); + this.updateOrder = 0; // Process game logic + } +} + +@ECSSystem('Render') +class RenderSystem extends EntitySystem { + constructor() { + super(); + this.updateOrder = 100; // Render last + } +} +``` + +### 4. Avoid Direct References Between Systems + +```typescript +// Avoid: Direct system references +@ECSSystem('Bad') +class BadSystem extends EntitySystem { + private otherSystem: SomeOtherSystem; // Avoid direct references to other systems +} + +// Recommended: Communicate through event system +@ECSSystem('Good') +class GoodSystem extends EntitySystem { + protected process(entities: readonly Entity[]): void { + // Communicate with other systems through event system + this.scene?.eventSystem.emitSync('data_updated', { entities }); + } +} +``` + +### 5. Clean Up Resources Promptly + +```typescript +@ECSSystem('Resource') +class ResourceSystem extends EntitySystem { + private resources: Map = new Map(); + + protected onDestroy(): void { + // Clean up resources + for (const [key, resource] of this.resources) { + if (resource.dispose) { + resource.dispose(); + } + } + this.resources.clear(); + } +} +``` + +Systems are the logic processing core of ECS architecture. Properly designing and using systems makes your game code more modular, efficient, and maintainable. diff --git a/docs/guide/system.md b/docs/guide/system.md index 50eb1c61..c5a4faa8 100644 --- a/docs/guide/system.md +++ b/docs/guide/system.md @@ -216,11 +216,13 @@ class ExampleSystem extends EntitySystem { // 主要的处理逻辑 for (const entity of entities) { // 处理每个实体 + // ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代 } } protected lateProcess(entities: readonly Entity[]): void { // 主处理之后的后期处理 + // ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代 } protected onEnd(): void { @@ -270,6 +272,68 @@ class EnemyManagerSystem extends EntitySystem { } ``` +### 重要:onAdded/onRemoved 的调用时机 + +> ⚠️ **注意**:`onAdded` 和 `onRemoved` 回调是**同步调用**的,会在 `addComponent`/`removeComponent` 返回**之前**立即执行。 + +这意味着: + +```typescript +// ❌ 错误的用法:链式赋值在 onAdded 之后才执行 +const comp = entity.addComponent(new ClickComponent()); +comp.element = this._element; // 此时 onAdded 已经执行完了! + +// ✅ 正确的用法:通过构造函数传入初始值 +const comp = entity.addComponent(new ClickComponent(this._element)); + +// ✅ 或者使用 createComponent 方法 +const comp = entity.createComponent(ClickComponent, this._element); +``` + +**为什么这样设计?** + +事件驱动设计确保 `onAdded`/`onRemoved` 回调不受系统注册顺序的影响。当组件被添加时,所有监听该组件的系统都会立即收到通知,而不是等到下一帧。 + +**最佳实践:** + +1. 组件的初始值应该通过**构造函数**传入 +2. 不要依赖 `addComponent` 返回后再设置属性 +3. 如果需要在 `onAdded` 中访问组件属性,确保这些属性在构造时已经设置 + +### 在 process/lateProcess 中安全地修改组件 + +在 `process` 或 `lateProcess` 中迭代实体时,可以安全地添加或移除组件,不会影响当前的迭代过程: + +```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; + + // ✅ 安全:移除组件不会影响当前迭代 + entity.removeComponent(damage); + + if (health.current <= 0) { + // ✅ 安全:添加组件也不会影响当前迭代 + entity.addComponent(new Dead()); + } + } + } + } +} +``` + +框架会在每次 `process`/`lateProcess` 调用前创建实体列表的快照,确保迭代过程中的组件变化不会导致跳过实体或重复处理。 + ## 系统属性和方法 ### 重要属性 @@ -457,6 +521,8 @@ class GameScene extends Scene { ### 系统更新顺序 +系统的执行顺序由 `updateOrder` 属性决定,数值越小越先执行: + ```typescript @ECSSystem('Input') class InputSystem extends EntitySystem { @@ -483,6 +549,25 @@ class RenderSystem extends EntitySystem { } ``` +#### 稳定排序:addOrder + +当多个系统的 `updateOrder` 相同时,框架使用 `addOrder`(添加顺序)作为第二排序条件,确保排序结果稳定可预测: + +```typescript +// 这两个系统 updateOrder 都是默认值 0 +@ECSSystem('SystemA') +class SystemA extends EntitySystem { /* ... */ } + +@ECSSystem('SystemB') +class SystemB extends EntitySystem { /* ... */ } + +// 添加顺序决定了执行顺序 +scene.addSystem(new SystemA()); // addOrder = 0,先执行 +scene.addSystem(new SystemB()); // addOrder = 1,后执行 +``` + +> **注意**:`addOrder` 由框架在 `addSystem` 时自动设置,无需手动管理。这确保了相同 `updateOrder` 的系统按照添加顺序执行,避免了排序不稳定导致的随机行为。 + ## 复杂系统示例 ### 碰撞检测系统 diff --git a/packages/core/src/ECS/Systems/EntitySystem.ts b/packages/core/src/ECS/Systems/EntitySystem.ts index cb6d9bb6..25d75d3d 100644 --- a/packages/core/src/ECS/Systems/EntitySystem.ts +++ b/packages/core/src/ECS/Systems/EntitySystem.ts @@ -601,10 +601,13 @@ export abstract class EntitySystem implements ISystemBase, IService { // 查询实体并存储到帧缓存中 // 响应式查询会自动维护最新的实体列表,updateEntityTracking会在检测到变化时invalidate const queriedEntities = this.queryEntities(); - this._entityCache.setFrame(queriedEntities); - entityCount = queriedEntities.length; + // 创建数组副本以防止迭代过程中数组被修改 + // Create a copy to prevent array modification during iteration + const entities = [...queriedEntities]; + this._entityCache.setFrame(entities); + entityCount = entities.length; - this.process(queriedEntities); + this.process(entities); } finally { monitor.endMonitoring(this._systemName, startTime, entityCount); } @@ -623,8 +626,15 @@ export abstract class EntitySystem implements ISystemBase, IService { let entityCount = 0; try { - // 使用缓存的实体列表,避免重复查询 - const entities = this._entityCache.getFrame() || []; + // 重新查询实体以获取最新列表 + // 在 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]; + this._entityCache.setFrame(entities); entityCount = entities.length; this.lateProcess(entities); this.onEnd(); diff --git a/packages/core/tests/ECS/Systems/EntitySystem.test.ts b/packages/core/tests/ECS/Systems/EntitySystem.test.ts index 04846493..476e985d 100644 --- a/packages/core/tests/ECS/Systems/EntitySystem.test.ts +++ b/packages/core/tests/ECS/Systems/EntitySystem.test.ts @@ -829,4 +829,256 @@ describe('EntitySystem', () => { }); }); + describe('addComponent 后立即 getComponent', () => { + it('addComponent 后应该能立即 getComponent 获取到组件', () => { + // 使用独立场景 | Use independent scene + const testScene = new Scene(); + + class ClickComponent extends Component { + public element: string; + constructor(element: string) { + super(); + this.element = element; + } + } + + const testEntity = testScene.createEntity('panel'); + + // 添加组件后立即获取 | Get component immediately after adding + const comp = testEntity.addComponent(new ClickComponent('button')); + const comp1 = testEntity.getComponent(ClickComponent); + + expect(comp).not.toBeNull(); + expect(comp1).not.toBeNull(); + expect(comp).toBe(comp1); + expect(comp1!.element).toBe('button'); + }); + + it('有系统监听时 addComponent 后应该能立即 getComponent', () => { + // 使用独立场景 | Use independent scene + const testScene = new Scene(); + + class ClickComponent extends Component { + public element: string; + constructor(element: string) { + super(); + this.element = element; + } + } + + // 添加一个监听该组件的系统 | Add a system that listens to this component + class ClickSystem extends EntitySystem { + public onAddedCount = 0; + + constructor() { + super(Matcher.all(ClickComponent)); + } + + protected override onAdded(entity: Entity): void { + this.onAddedCount++; + } + } + + const clickSystem = new ClickSystem(); + testScene.addSystem(clickSystem); + + const testEntity = testScene.createEntity('panel'); + + // 添加组件后立即获取 | Get component immediately after adding + const comp = testEntity.addComponent(new ClickComponent('button')); + const comp1 = testEntity.getComponent(ClickComponent); + + expect(comp).not.toBeNull(); + expect(comp1).not.toBeNull(); + expect(comp).toBe(comp1); + expect(comp1!.element).toBe('button'); + + // onAdded 应该被触发 | onAdded should be triggered + expect(clickSystem.onAddedCount).toBe(1); + + testScene.removeSystem(clickSystem); + }); + + it('系统在 onAdded 中移除组件时 getComponent 应返回 null', () => { + // 使用独立场景 | Use independent scene + const testScene = new Scene(); + + class ClickComponent extends Component { + public element: string; + constructor(element: string) { + super(); + this.element = element; + } + } + + // 这个系统在 onAdded 中移除组件(模拟可能的用户代码) + // This system removes component in onAdded (simulating possible user code) + class RemoveOnAddSystem extends EntitySystem { + constructor() { + super(Matcher.all(ClickComponent)); + } + + protected override onAdded(entity: Entity): void { + // 在 onAdded 中移除组件 | Remove component in onAdded + const comp = entity.getComponent(ClickComponent); + if (comp) { + entity.removeComponent(comp); + } + } + } + + const removeSystem = new RemoveOnAddSystem(); + testScene.addSystem(removeSystem); + + const testEntity = testScene.createEntity('panel'); + + // 添加组件 - 会触发 onAdded,然后组件被移除 + // Add component - triggers onAdded, then component is removed + const comp = testEntity.addComponent(new ClickComponent('button')); + + // 此时 getComponent 应该返回 null,因为组件在 onAdded 中被移除了 + // getComponent should return null because component was removed in onAdded + const comp1 = testEntity.getComponent(ClickComponent); + + expect(comp).not.toBeNull(); // addComponent 返回值仍然有效 + expect(comp1).toBeNull(); // 但 getComponent 返回 null + + testScene.removeSystem(removeSystem); + }); + + it('模拟 lawn-mower-demo: CSystem 在 process 中添加 D 组件', () => { + // 模拟 lawn-mower-demo 的场景 | Simulate lawn-mower-demo scenario + const testScene = new Scene(); + + // 组件定义 | Component definitions + class A extends Component {} + class B extends Component {} + class C extends Component { + public aId: number; + public bId: number; + constructor(aId: number, bId: number) { + super(); + this.aId = aId; + this.bId = bId; + } + } + class D extends Component {} + + // ASystem: 匹配 A + D | Matches A + D + class ASystem extends EntitySystem { + public onAddedEntities: Entity[] = []; + constructor() { + super(Matcher.all(A, D)); + } + protected override onAdded(entity: Entity): void { + console.log('ASystem onAdded:', entity.name); + this.onAddedEntities.push(entity); + } + } + + // BSystem: 匹配 B + D | Matches B + D + class BSystem extends EntitySystem { + public onAddedEntities: Entity[] = []; + constructor() { + super(Matcher.all(B, D)); + } + protected override onAdded(entity: Entity): void { + console.log('BSystem onAdded:', entity.name); + this.onAddedEntities.push(entity); + } + } + + // CSystem: 在 process 中给 A 和 B 实体添加 D 组件 + // CSystem: Adds D component to A and B entities in process + class CSystem extends EntitySystem { + constructor() { + super(Matcher.all(C)); + } + protected override process(entities: readonly Entity[]): void { + for (const entity of entities) { + const c = entity.getComponent(C); + if (c) { + const a = this.scene!.findEntityById(c.aId); + if (a && !a.hasComponent(D)) { + console.log('CSystem: Adding D to Entity A'); + a.addComponent(new D()); + } + const b = this.scene!.findEntityById(c.bId); + if (b && !b.hasComponent(D)) { + console.log('CSystem: Adding D to Entity B'); + b.addComponent(new D()); + } + } + } + } + } + + // DSystem: 在 lateProcess 中移除 D 组件 + // DSystem: Removes D component in lateProcess + class DSystem extends EntitySystem { + public lateProcessEntities: Entity[] = []; + constructor() { + super(Matcher.all(D)); + } + protected override lateProcess(entities: readonly Entity[]): void { + console.log('DSystem lateProcess, entities count:', entities.length); + for (const entity of entities) { + console.log('DSystem removing D from:', entity.name); + this.lateProcessEntities.push(entity); + const d = entity.getComponent(D); + if (d) { + entity.removeComponent(d); + } + } + } + } + + // 按顺序添加系统(与 demo 一致) + // Add systems in order (same as demo) + const aSystem = new ASystem(); + const bSystem = new BSystem(); + const cSystem = new CSystem(); + const dSystem = new DSystem(); + + testScene.addSystem(aSystem); + testScene.addSystem(bSystem); + testScene.addSystem(cSystem); + testScene.addSystem(dSystem); + + // 创建实体 | Create entities + const entity1 = testScene.createEntity('Entity1'); + entity1.addComponent(new A()); + + const entity2 = testScene.createEntity('Entity2'); + entity2.addComponent(new B()); + + const entity3 = testScene.createEntity('Entity3'); + entity3.addComponent(new C(entity1.id, entity2.id)); + + // 执行一帧 | Execute one frame + testScene.update(); + + // 验证 ASystem 和 BSystem 都收到了 onAdded 通知 + // Verify ASystem and BSystem both received onAdded notification + expect(aSystem.onAddedEntities.length).toBe(1); + expect(aSystem.onAddedEntities[0]).toBe(entity1); + + expect(bSystem.onAddedEntities.length).toBe(1); + expect(bSystem.onAddedEntities[0]).toBe(entity2); + + // 检查 DSystem 处理了哪些实体 + console.log('DSystem processed entities:', dSystem.lateProcessEntities.map(e => e.name)); + + // D 组件应该在 lateProcess 中被移除 + // D component should be removed in lateProcess + expect(entity1.hasComponent(D)).toBe(false); + expect(entity2.hasComponent(D)).toBe(false); + + testScene.removeSystem(aSystem); + testScene.removeSystem(bSystem); + testScene.removeSystem(cSystem); + testScene.removeSystem(dSystem); + }); + }); + }); \ No newline at end of file