From 690d7859c8608ec7a96639c76b5fdd13da257c7e Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Fri, 5 Dec 2025 22:54:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E5=AE=9E=E4=BD=93=E6=94=AF=E6=8C=81=E8=B7=A8?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E8=BF=81=E7=A7=BB=20(#285)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现实体生命周期策略,允许标记实体为持久化,在场景切换时自动迁移到新场景。 主要变更: - 新增 EEntityLifecyclePolicy 枚举(SceneLocal/Persistent) - Entity 添加 setPersistent()、setSceneLocal()、isPersistent API - Scene 添加 findPersistentEntities()、extractPersistentEntities()、receiveMigratedEntities() - SceneManager.setScene() 自动处理持久化实体迁移 - 添加完整的中英文文档和 21 个测试用例 --- docs/.vitepress/config.mjs | 3 +- docs/.vitepress/i18n/en.json | 1 + docs/.vitepress/i18n/zh.json | 1 + docs/en/guide/persistent-entity.md | 360 +++++++++++++++ docs/en/guide/scene-manager.md | 436 ++++++++++++++++++ docs/en/guide/scene.md | 364 +++++++++++++++ docs/guide/persistent-entity.md | 360 +++++++++++++++ docs/guide/scene-manager.md | 8 +- docs/guide/scene.md | 1 + .../src/ECS/Core/EntityLifecyclePolicy.ts | 26 ++ packages/core/src/ECS/Entity.ts | 63 +++ packages/core/src/ECS/Scene.ts | 75 +++ packages/core/src/ECS/SceneManager.ts | 40 +- packages/core/src/ECS/index.ts | 1 + .../core/tests/ECS/PersistentEntity.test.ts | 424 +++++++++++++++++ 15 files changed, 2158 insertions(+), 5 deletions(-) create mode 100644 docs/en/guide/persistent-entity.md create mode 100644 docs/en/guide/scene-manager.md create mode 100644 docs/en/guide/scene.md create mode 100644 docs/guide/persistent-entity.md create mode 100644 packages/core/src/ECS/Core/EntityLifecyclePolicy.ts create mode 100644 packages/core/tests/ECS/PersistentEntity.test.ts diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 8af807c9..3deec010 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -45,7 +45,8 @@ function createSidebar(t, prefix = '') { link: `${prefix}/guide/scene`, items: [ { text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` }, - { text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` } + { text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` }, + { text: t.sidebar.persistentEntity, link: `${prefix}/guide/persistent-entity` } ] }, { diff --git a/docs/.vitepress/i18n/en.json b/docs/.vitepress/i18n/en.json index 8db6f1a3..64be9e0c 100644 --- a/docs/.vitepress/i18n/en.json +++ b/docs/.vitepress/i18n/en.json @@ -23,6 +23,7 @@ "scene": "Scene", "sceneManager": "SceneManager", "worldManager": "WorldManager", + "persistentEntity": "Persistent Entity", "behaviorTree": "Behavior Tree", "btGettingStarted": "Getting Started", "btCoreConcepts": "Core Concepts", diff --git a/docs/.vitepress/i18n/zh.json b/docs/.vitepress/i18n/zh.json index 51d7ea34..4bd3e189 100644 --- a/docs/.vitepress/i18n/zh.json +++ b/docs/.vitepress/i18n/zh.json @@ -23,6 +23,7 @@ "scene": "场景管理 (Scene)", "sceneManager": "SceneManager", "worldManager": "WorldManager", + "persistentEntity": "持久化实体 (Persistent Entity)", "behaviorTree": "行为树系统 (Behavior Tree)", "btGettingStarted": "快速开始", "btCoreConcepts": "核心概念", diff --git a/docs/en/guide/persistent-entity.md b/docs/en/guide/persistent-entity.md new file mode 100644 index 00000000..b14455cf --- /dev/null +++ b/docs/en/guide/persistent-entity.md @@ -0,0 +1,360 @@ +# Persistent Entity + +> **Version**: v2.2.22+ + +Persistent Entity is a special type of entity that automatically migrates to the new scene during scene transitions. It is suitable for game objects that need to maintain state across scenes, such as players, game managers, audio managers, etc. + +## Basic Concepts + +In the ECS framework, entities have two lifecycle policies: + +| Policy | Description | Default | +|--------|-------------|---------| +| `SceneLocal` | Scene-local entity, destroyed when scene changes | ✓ | +| `Persistent` | Persistent entity, automatically migrates during scene transitions | | + +## Quick Start + +### Creating a Persistent Entity + +```typescript +import { Scene } from '@esengine/ecs-framework'; + +class GameScene extends Scene { + protected initialize(): void { + // Create a persistent player entity + const player = this.createEntity('Player').setPersistent(); + player.addComponent(new Position(100, 200)); + player.addComponent(new PlayerData('Hero', 500)); + + // Create a normal enemy entity (destroyed when scene changes) + const enemy = this.createEntity('Enemy'); + enemy.addComponent(new Position(300, 200)); + enemy.addComponent(new EnemyAI()); + } +} +``` + +### Behavior During Scene Transitions + +```typescript +import { Core, Scene } from '@esengine/ecs-framework'; + +// Initial scene +class Level1Scene extends Scene { + protected initialize(): void { + // Player - persistent, will migrate to the next scene + const player = this.createEntity('Player').setPersistent(); + player.addComponent(new Position(0, 0)); + player.addComponent(new Health(100)); + + // Enemy - scene-local, destroyed when scene changes + const enemy = this.createEntity('Enemy'); + enemy.addComponent(new Position(100, 100)); + } +} + +// Target scene +class Level2Scene extends Scene { + protected initialize(): void { + // New enemy + const enemy = this.createEntity('Boss'); + enemy.addComponent(new Position(200, 200)); + } + + public onStart(): void { + // Player has automatically migrated to this scene + const player = this.findEntity('Player'); + console.log(player !== null); // true + + // Position and health data are fully preserved + const position = player?.getComponent(Position); + const health = player?.getComponent(Health); + console.log(position?.x, position?.y); // 0, 0 + console.log(health?.value); // 100 + } +} + +// Switch scenes +Core.create({ debug: true }); +Core.setScene(new Level1Scene()); + +// Later switch to Level2 +Core.loadScene(new Level2Scene()); +// Player entity migrates automatically, Enemy entity is destroyed +``` + +## API Reference + +### Entity Methods + +#### setPersistent() + +Marks the entity as persistent, preventing destruction during scene transitions. + +```typescript +public setPersistent(): this +``` + +**Returns**: Returns the entity itself for method chaining + +**Example**: +```typescript +const player = scene.createEntity('Player') + .setPersistent(); + +player.addComponent(new Position(100, 200)); +``` + +#### setSceneLocal() + +Restores the entity to scene-local policy (default). + +```typescript +public setSceneLocal(): this +``` + +**Returns**: Returns the entity itself for method chaining + +**Example**: +```typescript +// Dynamically cancel persistence +player.setSceneLocal(); +``` + +#### isPersistent + +Checks if the entity is persistent. + +```typescript +public get isPersistent(): boolean +``` + +**Example**: +```typescript +if (entity.isPersistent) { + console.log('This is a persistent entity'); +} +``` + +#### lifecyclePolicy + +Gets the entity's lifecycle policy. + +```typescript +public get lifecyclePolicy(): EEntityLifecyclePolicy +``` + +**Example**: +```typescript +import { EEntityLifecyclePolicy } from '@esengine/ecs-framework'; + +if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) { + console.log('Persistent entity'); +} +``` + +### Scene Methods + +#### findPersistentEntities() + +Finds all persistent entities in the scene. + +```typescript +public findPersistentEntities(): Entity[] +``` + +**Returns**: Array of persistent entities + +**Example**: +```typescript +const persistentEntities = scene.findPersistentEntities(); +console.log(`Scene has ${persistentEntities.length} persistent entities`); +``` + +#### extractPersistentEntities() + +Extracts and removes all persistent entities from the scene (typically called internally by the framework). + +```typescript +public extractPersistentEntities(): Entity[] +``` + +**Returns**: Array of extracted persistent entities + +#### receiveMigratedEntities() + +Receives migrated entities (typically called internally by the framework). + +```typescript +public receiveMigratedEntities(entities: Entity[]): void +``` + +**Parameters**: +- `entities` - Array of entities to receive + +## Use Cases + +### 1. Player Entity Across Levels + +```typescript +class PlayerSetupScene extends Scene { + protected initialize(): void { + // Player maintains state across all levels + const player = this.createEntity('Player').setPersistent(); + player.addComponent(new Transform(0, 0)); + player.addComponent(new Health(100)); + player.addComponent(new Inventory()); + player.addComponent(new PlayerStats()); + } +} + +class Level1 extends Scene { /* ... */ } +class Level2 extends Scene { /* ... */ } +class Level3 extends Scene { /* ... */ } + +// Player entity automatically migrates between all levels +Core.setScene(new PlayerSetupScene()); +// ... game progresses +Core.loadScene(new Level1()); +// ... level complete +Core.loadScene(new Level2()); +// Player data (health, inventory, stats) fully preserved +``` + +### 2. Global Managers + +```typescript +class BootstrapScene extends Scene { + protected initialize(): void { + // Audio manager - persists across scenes + const audioManager = this.createEntity('AudioManager').setPersistent(); + audioManager.addComponent(new AudioController()); + + // Achievement manager - persists across scenes + const achievementManager = this.createEntity('AchievementManager').setPersistent(); + achievementManager.addComponent(new AchievementTracker()); + + // Game settings - persists across scenes + const settings = this.createEntity('GameSettings').setPersistent(); + settings.addComponent(new SettingsData()); + } +} +``` + +### 3. Dynamically Toggling Persistence + +```typescript +class GameScene extends Scene { + protected initialize(): void { + // Initially created as a normal entity + const companion = this.createEntity('Companion'); + companion.addComponent(new Transform(0, 0)); + companion.addComponent(new CompanionAI()); + + // Listen for recruitment event + this.eventSystem.on('companion:recruited', () => { + // After recruitment, become persistent + companion.setPersistent(); + console.log('Companion joined the party, will follow player across scenes'); + }); + + // Listen for dismissal event + this.eventSystem.on('companion:dismissed', () => { + // After dismissal, restore to scene-local + companion.setSceneLocal(); + console.log('Companion left the party, will no longer persist across scenes'); + }); + } +} +``` + +## Best Practices + +### 1. Clearly Identify Persistent Entities + +```typescript +// Recommended: Mark immediately when creating +const player = this.createEntity('Player').setPersistent(); + +// Not recommended: Marking after creation (easy to forget) +const player = this.createEntity('Player'); +// ... lots of code ... +player.setPersistent(); // Easy to forget +``` + +### 2. Use Persistence Appropriately + +```typescript +// ✓ Entities suitable for persistence +const player = this.createEntity('Player').setPersistent(); // Player +const gameManager = this.createEntity('GameManager').setPersistent(); // Global manager +const audioManager = this.createEntity('AudioManager').setPersistent(); // Audio system + +// ✗ Entities that should NOT be persistent +const bullet = this.createEntity('Bullet'); // Temporary objects +const enemy = this.createEntity('Enemy'); // Level-specific enemies +const particle = this.createEntity('Particle'); // Effect particles +``` + +### 3. Check Migrated Entities + +```typescript +class NewScene extends Scene { + public onStart(): void { + // Check if expected persistent entities exist + const player = this.findEntity('Player'); + if (!player) { + console.error('Player entity did not migrate correctly!'); + // Handle error case + } + } +} +``` + +### 4. Avoid Circular References + +```typescript +// ✗ Avoid: Persistent entity referencing scene-local entity +class BadScene extends Scene { + protected initialize(): void { + const player = this.createEntity('Player').setPersistent(); + const enemy = this.createEntity('Enemy'); + + // Dangerous: player is persistent but enemy is not + // After scene change, enemy is destroyed, reference becomes invalid + player.addComponent(new TargetComponent(enemy)); + } +} + +// ✓ Recommended: Use ID references or event system +class GoodScene extends Scene { + protected initialize(): void { + const player = this.createEntity('Player').setPersistent(); + const enemy = this.createEntity('Enemy'); + + // Store ID instead of direct reference + player.addComponent(new TargetComponent(enemy.id)); + + // Or use event system for communication + } +} +``` + +## Important Notes + +1. **Destroyed entities will not migrate**: If an entity is destroyed before scene transition, it will not migrate even if marked as persistent. + +2. **Component data is fully preserved**: All components and their state are preserved during migration. + +3. **Scene reference is updated**: After migration, the entity's `scene` property will point to the new scene. + +4. **Query system is updated**: Migrated entities are automatically registered in the new scene's query system. + +5. **Delayed transitions also work**: Persistent entities migrate when using `Core.loadScene()` for delayed transitions as well. + +## Related Documentation + +- [Scene](./scene) - Learn the basics of scenes +- [SceneManager](./scene-manager) - Learn about scene transitions +- [WorldManager](./world-manager) - Learn about multi-world management diff --git a/docs/en/guide/scene-manager.md b/docs/en/guide/scene-manager.md new file mode 100644 index 00000000..6aa27405 --- /dev/null +++ b/docs/en/guide/scene-manager.md @@ -0,0 +1,436 @@ +# SceneManager + +SceneManager is a lightweight scene manager provided by ECS Framework, suitable for 95% of game applications. It provides a simple and intuitive API with support for scene transitions and delayed loading. + +## Use Cases + +SceneManager is suitable for: +- Single-player games +- Simple multiplayer games +- Mobile games +- Games requiring scene transitions (menu, game, pause, etc.) +- Projects that don't need multi-World isolation + +## Features + +- Lightweight, zero extra overhead +- Simple and intuitive API +- Supports delayed scene transitions (avoids switching mid-frame) +- Automatic ECS fluent API management +- Automatic scene lifecycle handling +- Integrated with Core, auto-updated +- Supports [Persistent Entity](./persistent-entity) migration across scenes (v2.2.22+) + +## Basic Usage + +### Recommended: Using Core's Static Methods + +This is the simplest and recommended approach, suitable for most applications: + +```typescript +import { Core, Scene } from '@esengine/ecs-framework'; + +// 1. Initialize Core +Core.create({ debug: true }); + +// 2. Create and set scene +class GameScene extends Scene { + protected initialize(): void { + this.name = "GameScene"; + + // Add systems + this.addSystem(new MovementSystem()); + this.addSystem(new RenderSystem()); + + // Create initial entities + const player = this.createEntity("Player"); + player.addComponent(new Transform(400, 300)); + player.addComponent(new Health(100)); + } + + public onStart(): void { + console.log("Game scene started"); + } +} + +// 3. Set scene +Core.setScene(new GameScene()); + +// 4. Game loop (Core.update automatically updates the scene) +function gameLoop(deltaTime: number) { + Core.update(deltaTime); // Automatically updates all services and scenes +} + +// Laya engine integration +Laya.timer.frameLoop(1, this, () => { + const deltaTime = Laya.timer.delta / 1000; + Core.update(deltaTime); +}); + +// Cocos Creator integration +update(deltaTime: number) { + Core.update(deltaTime); +} +``` + +### Advanced: Using SceneManager Directly + +If you need more control, you can use SceneManager directly: + +```typescript +import { Core, SceneManager, Scene } from '@esengine/ecs-framework'; + +// Initialize Core +Core.create({ debug: true }); + +// Get SceneManager (already auto-created and registered by Core) +const sceneManager = Core.services.resolve(SceneManager); + +// Set scene +const gameScene = new GameScene(); +sceneManager.setScene(gameScene); + +// Game loop (still use Core.update) +function gameLoop(deltaTime: number) { + Core.update(deltaTime); // Core automatically calls sceneManager.update() +} +``` + +**Important**: Regardless of which approach you use, you should only call `Core.update()` in the game loop. It automatically updates SceneManager and scenes. You don't need to manually call `sceneManager.update()`. + +## Scene Transitions + +### Immediate Transition + +Use `Core.setScene()` or `sceneManager.setScene()` to immediately switch scenes: + +```typescript +// Method 1: Using Core (recommended) +Core.setScene(new MenuScene()); + +// Method 2: Using SceneManager +const sceneManager = Core.services.resolve(SceneManager); +sceneManager.setScene(new MenuScene()); +``` + +### Delayed Transition + +Use `Core.loadScene()` or `sceneManager.loadScene()` for delayed scene transition, which takes effect on the next frame: + +```typescript +// Method 1: Using Core (recommended) +Core.loadScene(new GameOverScene()); + +// Method 2: Using SceneManager +const sceneManager = Core.services.resolve(SceneManager); +sceneManager.loadScene(new GameOverScene()); +``` + +When switching scenes from within a System, use delayed transitions: + +```typescript +class GameOverSystem extends EntitySystem { + process(entities: readonly Entity[]): void { + const player = entities.find(e => e.name === 'Player'); + const health = player?.getComponent(Health); + + if (health && health.value <= 0) { + // Delayed transition to game over scene (takes effect next frame) + Core.loadScene(new GameOverScene()); + // Current frame continues execution, won't interrupt current system processing + } + } +} +``` + +## API Reference + +### Core Static Methods (Recommended) + +#### Core.setScene() + +Immediately switch scenes. + +```typescript +public static setScene(scene: T): T +``` + +**Parameters**: +- `scene` - The scene instance to set + +**Returns**: +- Returns the set scene instance + +**Example**: +```typescript +const gameScene = Core.setScene(new GameScene()); +console.log(gameScene.name); +``` + +#### Core.loadScene() + +Delayed scene loading (switches on next frame). + +```typescript +public static loadScene(scene: T): void +``` + +**Parameters**: +- `scene` - The scene instance to load + +**Example**: +```typescript +Core.loadScene(new GameOverScene()); +``` + +#### Core.scene + +Get the currently active scene. + +```typescript +public static get scene(): IScene | null +``` + +**Returns**: +- Current scene instance, or null if no scene + +**Example**: +```typescript +const currentScene = Core.scene; +if (currentScene) { + console.log(`Current scene: ${currentScene.name}`); +} +``` + +### SceneManager Methods (Advanced) + +If you need to use SceneManager directly, get it through the service container: + +```typescript +const sceneManager = Core.services.resolve(SceneManager); +``` + +#### setScene() + +Immediately switch scenes. + +```typescript +public setScene(scene: T): T +``` + +#### loadScene() + +Delayed scene loading. + +```typescript +public loadScene(scene: T): void +``` + +#### currentScene + +Get the current scene. + +```typescript +public get currentScene(): IScene | null +``` + +#### hasScene + +Check if there's an active scene. + +```typescript +public get hasScene(): boolean +``` + +#### hasPendingScene + +Check if there's a pending scene transition. + +```typescript +public get hasPendingScene(): boolean +``` + +## Best Practices + +### 1. Use Core's Static Methods + +```typescript +// Recommended: Use Core's static methods +Core.setScene(new GameScene()); +Core.loadScene(new MenuScene()); +const currentScene = Core.scene; + +// Not recommended: Don't directly use SceneManager unless you have special needs +const sceneManager = Core.services.resolve(SceneManager); +sceneManager.setScene(new GameScene()); +``` + +### 2. Only Call Core.update() + +```typescript +// Correct: Only call Core.update() +function gameLoop(deltaTime: number) { + Core.update(deltaTime); // Automatically updates all services and scenes +} + +// Incorrect: Don't manually call sceneManager.update() +function gameLoop(deltaTime: number) { + Core.update(deltaTime); + sceneManager.update(); // Duplicate update, will cause issues! +} +``` + +### 3. Use Delayed Transitions to Avoid Issues + +When switching scenes from within a System, use `loadScene()` instead of `setScene()`: + +```typescript +// Recommended: Delayed transition +class HealthSystem extends EntitySystem { + process(entities: readonly Entity[]): void { + for (const entity of entities) { + const health = entity.getComponent(Health); + if (health.value <= 0) { + Core.loadScene(new GameOverScene()); + // Current frame continues processing other entities + } + } + } +} + +// Not recommended: Immediate transition may cause issues +class HealthSystem extends EntitySystem { + process(entities: readonly Entity[]): void { + for (const entity of entities) { + const health = entity.getComponent(Health); + if (health.value <= 0) { + Core.setScene(new GameOverScene()); + // Scene switches immediately, other entities in current frame may not process correctly + } + } + } +} +``` + +### 4. Scene Responsibility Separation + +Each scene should be responsible for only one specific game state: + +```typescript +// Good design - clear responsibilities +class MenuScene extends Scene { + // Only handles menu-related logic +} + +class GameScene extends Scene { + // Only handles gameplay logic +} + +class PauseScene extends Scene { + // Only handles pause screen logic +} + +// Avoid this design - mixed responsibilities +class MegaScene extends Scene { + // Contains menu, game, pause, and all other logic +} +``` + +### 5. Resource Management + +Clean up resources in the scene's `unload()` method: + +```typescript +class GameScene extends Scene { + private textures: Map = new Map(); + private sounds: Map = new Map(); + + protected initialize(): void { + this.loadResources(); + } + + private loadResources(): void { + this.textures.set('player', loadTexture('player.png')); + this.sounds.set('bgm', loadSound('bgm.mp3')); + } + + public unload(): void { + // Cleanup resources + this.textures.clear(); + this.sounds.clear(); + console.log('Scene resources cleaned up'); + } +} +``` + +### 6. Event-Driven Scene Transitions + +Use the event system to trigger scene transitions, keeping code decoupled: + +```typescript +class GameScene extends Scene { + protected initialize(): void { + // Listen to scene transition events + this.eventSystem.on('goto:menu', () => { + Core.loadScene(new MenuScene()); + }); + + this.eventSystem.on('goto:gameover', (data) => { + Core.loadScene(new GameOverScene()); + }); + } +} + +// Trigger events in System +class GameLogicSystem extends EntitySystem { + process(entities: readonly Entity[]): void { + if (levelComplete) { + this.scene.eventSystem.emitSync('goto:gameover', { + score: 1000, + level: 5 + }); + } + } +} +``` + +## Architecture Overview + +SceneManager's position in ECS Framework: + +``` +Core (Global Services) + └── SceneManager (Scene Management, auto-updated) + └── Scene (Current Scene) + ├── EntitySystem (Systems) + ├── Entity (Entities) + └── Component (Components) +``` + +## Comparison with WorldManager + +| Feature | SceneManager | WorldManager | +|---------|--------------|--------------| +| Use Case | 95% of game applications | Advanced multi-world isolation scenarios | +| Complexity | Simple | Complex | +| Scene Count | Single scene (switchable) | Multiple Worlds, each with multiple scenes | +| Performance Overhead | Minimal | Higher | +| Usage | `Core.setScene()` | `worldManager.createWorld()` | + +**When to use SceneManager**: +- Single-player games +- Simple multiplayer games +- Mobile games +- Scenes that need transitions but don't need to run simultaneously + +**When to use WorldManager**: +- MMO game servers (one World per room) +- Game lobby systems (complete isolation per game room) +- Need to run multiple completely independent game instances + +## Related Documentation + +- [Persistent Entity](./persistent-entity) - Learn how to keep entities across scene transitions +- [WorldManager](./world-manager) - Learn about advanced multi-world isolation features + +SceneManager provides simple yet powerful scene management capabilities for most games. Through Core's static methods, you can easily manage scene transitions. diff --git a/docs/en/guide/scene.md b/docs/en/guide/scene.md new file mode 100644 index 00000000..ed458468 --- /dev/null +++ b/docs/en/guide/scene.md @@ -0,0 +1,364 @@ +# Scene Management + +In the ECS architecture, a Scene is a container for the game world, responsible for managing the lifecycle of entities, systems, and components. Scenes provide a complete ECS runtime environment. + +## Basic Concepts + +Scene is the core container of the ECS framework, providing: +- Entity creation, management, and destruction +- System registration and execution scheduling +- Component storage and querying +- Event system support +- Performance monitoring and debugging information + +## Scene Management Options + +ECS Framework provides two scene management approaches: + +1. **[SceneManager](./scene-manager)** - Suitable for 95% of game applications + - Single-player games, simple multiplayer games, mobile games + - Lightweight, simple and intuitive API + - Supports scene transitions + +2. **[WorldManager](./world-manager)** - Suitable for advanced multi-world isolation scenarios + - MMO game servers, game room systems + - Multi-World management, each World can contain multiple scenes + - Completely isolated independent environments + +This document focuses on the usage of the Scene class itself. For detailed information about scene managers, please refer to the corresponding documentation. + +## Creating a Scene + +### Inheriting the Scene Class + +**Recommended: Inherit the Scene class to create custom scenes** + +```typescript +import { Scene, EntitySystem } from '@esengine/ecs-framework'; + +class GameScene extends Scene { + protected initialize(): void { + // Set scene name + this.name = "GameScene"; + + // Add systems + this.addSystem(new MovementSystem()); + this.addSystem(new RenderSystem()); + this.addSystem(new PhysicsSystem()); + + // Create initial entities + this.createInitialEntities(); + } + + private createInitialEntities(): void { + // Create player + const player = this.createEntity("Player"); + player.addComponent(new Position(400, 300)); + player.addComponent(new Health(100)); + player.addComponent(new PlayerController()); + + // Create enemies + for (let i = 0; i < 5; i++) { + const enemy = this.createEntity(`Enemy_${i}`); + enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600)); + enemy.addComponent(new Health(50)); + enemy.addComponent(new EnemyAI()); + } + } + + public onStart(): void { + console.log("Game scene started"); + // Logic when scene starts + } + + public unload(): void { + console.log("Game scene unloaded"); + // Cleanup logic when scene unloads + } +} +``` + +### Using Scene Configuration + +```typescript +import { ISceneConfig } from '@esengine/ecs-framework'; + +const config: ISceneConfig = { + name: "MainGame", + enableEntityDirectUpdate: false +}; + +class ConfiguredScene extends Scene { + constructor() { + super(config); + } +} +``` + +## Scene Lifecycle + +Scene provides complete lifecycle management: + +```typescript +class ExampleScene extends Scene { + protected initialize(): void { + // Scene initialization: setup systems and initial entities + console.log("Scene initializing"); + } + + public onStart(): void { + // Scene starts running: game logic begins execution + console.log("Scene starting"); + } + + public unload(): void { + // Scene unloading: cleanup resources + console.log("Scene unloading"); + } +} + +// Using scenes (lifecycle automatically managed by framework) +const scene = new ExampleScene(); +// Scene's initialize(), begin(), update(), end() are automatically called by the framework +``` + +**Lifecycle Methods**: + +1. `initialize()` - Scene initialization, setup systems and initial entities +2. `begin()` / `onStart()` - Scene starts running +3. `update()` - Per-frame update (called by scene manager) +4. `end()` / `unload()` - Scene unloading, cleanup resources + +## Entity Management + +### Creating Entities + +```typescript +class EntityScene extends Scene { + createGameEntities(): void { + // Create single entity + const player = this.createEntity("Player"); + + // Batch create entities (high performance) + const bullets = this.createEntities(100, "Bullet"); + + // Add components to batch-created entities + bullets.forEach((bullet, index) => { + bullet.addComponent(new Position(index * 10, 100)); + bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300)); + }); + } +} +``` + +### Finding Entities + +```typescript +class SearchScene extends Scene { + findEntities(): void { + // Find by name + const player = this.findEntity("Player"); + const player2 = this.getEntityByName("Player"); // Alias method + + // Find by ID + const entity = this.findEntityById(123); + + // Find by tag + const enemies = this.findEntitiesByTag(2); + const enemies2 = this.getEntitiesByTag(2); // Alias method + + if (player) { + console.log(`Found player: ${player.name}`); + } + + console.log(`Found ${enemies.length} enemies`); + } +} +``` + +### Destroying Entities + +```typescript +class DestroyScene extends Scene { + cleanupEntities(): void { + // Destroy all entities + this.destroyAllEntities(); + + // Single entity destruction through the entity itself + const enemy = this.findEntity("Enemy_1"); + if (enemy) { + enemy.destroy(); // Entity is automatically removed from the scene + } + } +} +``` + +## System Management + +### Adding and Removing Systems + +```typescript +class SystemScene extends Scene { + protected initialize(): void { + // Add systems + const movementSystem = new MovementSystem(); + this.addSystem(movementSystem); + + // Set system update order + movementSystem.updateOrder = 1; + + // Add more systems + this.addSystem(new PhysicsSystem()); + this.addSystem(new RenderSystem()); + } + + public removeUnnecessarySystems(): void { + // Get system + const physicsSystem = this.getEntityProcessor(PhysicsSystem); + + // Remove system + if (physicsSystem) { + this.removeSystem(physicsSystem); + } + } +} +``` + +## Event System + +Scene has a built-in type-safe event system: + +```typescript +class EventScene extends Scene { + protected initialize(): void { + // Listen to events + this.eventSystem.on('player_died', this.onPlayerDied.bind(this)); + this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this)); + this.eventSystem.on('level_complete', this.onLevelComplete.bind(this)); + } + + private onPlayerDied(data: any): void { + console.log('Player died event'); + // Handle player death + } + + private onEnemySpawned(data: any): void { + console.log('Enemy spawned event'); + // Handle enemy spawn + } + + private onLevelComplete(data: any): void { + console.log('Level complete event'); + // Handle level completion + } + + public triggerGameEvent(): void { + // Send event (synchronous) + this.eventSystem.emitSync('custom_event', { + message: "This is a custom event", + timestamp: Date.now() + }); + + // Send event (asynchronous) + this.eventSystem.emit('async_event', { + data: "Async event data" + }); + } +} +``` + +## Best Practices + +### 1. Scene Responsibility Separation + +```typescript +// Good scene design - clear responsibilities +class MenuScene extends Scene { + // Only handles menu-related logic +} + +class GameScene extends Scene { + // Only handles gameplay logic +} + +class InventoryScene extends Scene { + // Only handles inventory logic +} + +// Avoid this design - mixed responsibilities +class MegaScene extends Scene { + // Contains menu, game, inventory, and all other logic +} +``` + +### 2. Proper System Organization + +```typescript +class OrganizedScene extends Scene { + protected initialize(): void { + // Add systems by function and dependencies + this.addInputSystems(); + this.addLogicSystems(); + this.addRenderSystems(); + } + + private addInputSystems(): void { + this.addSystem(new InputSystem()); + } + + private addLogicSystems(): void { + this.addSystem(new MovementSystem()); + this.addSystem(new PhysicsSystem()); + this.addSystem(new CollisionSystem()); + } + + private addRenderSystems(): void { + this.addSystem(new RenderSystem()); + this.addSystem(new UISystem()); + } +} +``` + +### 3. Resource Management + +```typescript +class ResourceScene extends Scene { + private textures: Map = new Map(); + private sounds: Map = new Map(); + + protected initialize(): void { + this.loadResources(); + } + + private loadResources(): void { + // Load resources needed by the scene + this.textures.set('player', this.loadTexture('player.png')); + this.sounds.set('bgm', this.loadSound('bgm.mp3')); + } + + public unload(): void { + // Cleanup resources + this.textures.clear(); + this.sounds.clear(); + console.log('Scene resources cleaned up'); + } + + private loadTexture(path: string): any { + // Load texture + return null; + } + + private loadSound(path: string): any { + // Load sound + return null; + } +} +``` + +## Next Steps + +- Learn about [SceneManager](./scene-manager) - Simple scene management for most games +- Learn about [WorldManager](./world-manager) - For scenarios requiring multi-world isolation +- Learn about [Persistent Entity](./persistent-entity) - Keep entities across scene transitions (v2.2.22+) + +Scene is the core container of the ECS framework. Proper scene management makes your game architecture clearer, more modular, and easier to maintain. diff --git a/docs/guide/persistent-entity.md b/docs/guide/persistent-entity.md new file mode 100644 index 00000000..c49206e7 --- /dev/null +++ b/docs/guide/persistent-entity.md @@ -0,0 +1,360 @@ +# 持久化实体 + +> **版本**: v2.2.22+ + +持久化实体(Persistent Entity)是一种可以在场景切换时自动迁移到新场景的特殊实体。适用于需要跨场景保持状态的游戏对象,如玩家、游戏管理器、音频管理器等。 + +## 基本概念 + +在 ECS 框架中,实体有两种生命周期策略: + +| 策略 | 说明 | 默认 | +|-----|------|------| +| `SceneLocal` | 场景本地实体,场景切换时销毁 | ✓ | +| `Persistent` | 持久化实体,场景切换时自动迁移 | | + +## 快速开始 + +### 创建持久化实体 + +```typescript +import { Scene } from '@esengine/ecs-framework'; + +class GameScene extends Scene { + protected initialize(): void { + // 创建持久化玩家实体 + const player = this.createEntity('Player').setPersistent(); + player.addComponent(new Position(100, 200)); + player.addComponent(new PlayerData('Hero', 500)); + + // 创建普通敌人实体(场景切换时销毁) + const enemy = this.createEntity('Enemy'); + enemy.addComponent(new Position(300, 200)); + enemy.addComponent(new EnemyAI()); + } +} +``` + +### 场景切换时的行为 + +```typescript +import { Core, Scene } from '@esengine/ecs-framework'; + +// 初始场景 +class Level1Scene extends Scene { + protected initialize(): void { + // 玩家 - 持久化,会迁移到下一个场景 + const player = this.createEntity('Player').setPersistent(); + player.addComponent(new Position(0, 0)); + player.addComponent(new Health(100)); + + // 敌人 - 场景本地,切换时销毁 + const enemy = this.createEntity('Enemy'); + enemy.addComponent(new Position(100, 100)); + } +} + +// 目标场景 +class Level2Scene extends Scene { + protected initialize(): void { + // 新的敌人 + const enemy = this.createEntity('Boss'); + enemy.addComponent(new Position(200, 200)); + } + + public onStart(): void { + // 玩家已自动迁移到此场景 + const player = this.findEntity('Player'); + console.log(player !== null); // true + + // 位置和血量数据完整保留 + const position = player?.getComponent(Position); + const health = player?.getComponent(Health); + console.log(position?.x, position?.y); // 0, 0 + console.log(health?.value); // 100 + } +} + +// 切换场景 +Core.create({ debug: true }); +Core.setScene(new Level1Scene()); + +// 稍后切换到 Level2 +Core.loadScene(new Level2Scene()); +// Player 实体自动迁移,Enemy 实体被销毁 +``` + +## API 参考 + +### Entity 方法 + +#### setPersistent() + +将实体标记为持久化,场景切换时不会被销毁。 + +```typescript +public setPersistent(): this +``` + +**返回**: 返回实体本身,支持链式调用 + +**示例**: +```typescript +const player = scene.createEntity('Player') + .setPersistent(); + +player.addComponent(new Position(100, 200)); +``` + +#### setSceneLocal() + +将实体恢复为场景本地策略(默认)。 + +```typescript +public setSceneLocal(): this +``` + +**返回**: 返回实体本身,支持链式调用 + +**示例**: +```typescript +// 动态取消持久化 +player.setSceneLocal(); +``` + +#### isPersistent + +检查实体是否为持久化实体。 + +```typescript +public get isPersistent(): boolean +``` + +**示例**: +```typescript +if (entity.isPersistent) { + console.log('这是持久化实体'); +} +``` + +#### lifecyclePolicy + +获取实体的生命周期策略。 + +```typescript +public get lifecyclePolicy(): EEntityLifecyclePolicy +``` + +**示例**: +```typescript +import { EEntityLifecyclePolicy } from '@esengine/ecs-framework'; + +if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) { + console.log('持久化实体'); +} +``` + +### Scene 方法 + +#### findPersistentEntities() + +查找场景中所有持久化实体。 + +```typescript +public findPersistentEntities(): Entity[] +``` + +**返回**: 持久化实体数组 + +**示例**: +```typescript +const persistentEntities = scene.findPersistentEntities(); +console.log(`场景中有 ${persistentEntities.length} 个持久化实体`); +``` + +#### extractPersistentEntities() + +提取并从场景中移除所有持久化实体(通常由框架内部调用)。 + +```typescript +public extractPersistentEntities(): Entity[] +``` + +**返回**: 被提取的持久化实体数组 + +#### receiveMigratedEntities() + +接收迁移过来的实体(通常由框架内部调用)。 + +```typescript +public receiveMigratedEntities(entities: Entity[]): void +``` + +**参数**: +- `entities` - 要接收的实体数组 + +## 使用场景 + +### 1. 玩家实体跨关卡 + +```typescript +class PlayerSetupScene extends Scene { + protected initialize(): void { + // 玩家在所有关卡中保持状态 + const player = this.createEntity('Player').setPersistent(); + player.addComponent(new Transform(0, 0)); + player.addComponent(new Health(100)); + player.addComponent(new Inventory()); + player.addComponent(new PlayerStats()); + } +} + +class Level1 extends Scene { /* ... */ } +class Level2 extends Scene { /* ... */ } +class Level3 extends Scene { /* ... */ } + +// 玩家实体会自动在所有关卡间迁移 +Core.setScene(new PlayerSetupScene()); +// ... 游戏进行 +Core.loadScene(new Level1()); +// ... 关卡完成 +Core.loadScene(new Level2()); +// 玩家数据(血量、物品栏、属性)完整保留 +``` + +### 2. 全局管理器 + +```typescript +class BootstrapScene extends Scene { + protected initialize(): void { + // 音频管理器 - 跨场景保持 + const audioManager = this.createEntity('AudioManager').setPersistent(); + audioManager.addComponent(new AudioController()); + + // 成就管理器 - 跨场景保持 + const achievementManager = this.createEntity('AchievementManager').setPersistent(); + achievementManager.addComponent(new AchievementTracker()); + + // 游戏设置 - 跨场景保持 + const settings = this.createEntity('GameSettings').setPersistent(); + settings.addComponent(new SettingsData()); + } +} +``` + +### 3. 动态切换持久化状态 + +```typescript +class GameScene extends Scene { + protected initialize(): void { + // 初始创建为普通实体 + const companion = this.createEntity('Companion'); + companion.addComponent(new Transform(0, 0)); + companion.addComponent(new CompanionAI()); + + // 监听招募事件 + this.eventSystem.on('companion:recruited', () => { + // 招募后变为持久化实体 + companion.setPersistent(); + console.log('同伴已加入队伍,将跟随玩家跨场景'); + }); + + // 监听解散事件 + this.eventSystem.on('companion:dismissed', () => { + // 解散后恢复为场景本地实体 + companion.setSceneLocal(); + console.log('同伴已离队,不再跨场景'); + }); + } +} +``` + +## 最佳实践 + +### 1. 明确标识持久化实体 + +```typescript +// 推荐:在创建时立即标记 +const player = this.createEntity('Player').setPersistent(); + +// 不推荐:创建后再标记(容易遗漏) +const player = this.createEntity('Player'); +// ... 很多代码 ... +player.setPersistent(); // 容易忘记 +``` + +### 2. 合理使用持久化 + +```typescript +// ✓ 适合持久化的实体 +const player = this.createEntity('Player').setPersistent(); // 玩家 +const gameManager = this.createEntity('GameManager').setPersistent(); // 全局管理器 +const audioManager = this.createEntity('AudioManager').setPersistent(); // 音频系统 + +// ✗ 不应持久化的实体 +const bullet = this.createEntity('Bullet'); // 临时对象 +const enemy = this.createEntity('Enemy'); // 关卡特定敌人 +const particle = this.createEntity('Particle'); // 特效粒子 +``` + +### 3. 检查迁移后的实体 + +```typescript +class NewScene extends Scene { + public onStart(): void { + // 检查预期的持久化实体是否存在 + const player = this.findEntity('Player'); + if (!player) { + console.error('玩家实体未正确迁移!'); + // 处理错误情况 + } + } +} +``` + +### 4. 避免循环引用 + +```typescript +// ✗ 避免:持久化实体引用场景本地实体 +class BadScene extends Scene { + protected initialize(): void { + const player = this.createEntity('Player').setPersistent(); + const enemy = this.createEntity('Enemy'); + + // 危险:player 持久化但 enemy 不是 + // 场景切换后 enemy 被销毁,引用失效 + player.addComponent(new TargetComponent(enemy)); + } +} + +// ✓ 推荐:使用 ID 引用或事件系统 +class GoodScene extends Scene { + protected initialize(): void { + const player = this.createEntity('Player').setPersistent(); + const enemy = this.createEntity('Enemy'); + + // 存储 ID 而非直接引用 + player.addComponent(new TargetComponent(enemy.id)); + + // 或使用事件系统通信 + } +} +``` + +## 注意事项 + +1. **已销毁的实体不会迁移**:如果实体在场景切换前被销毁,即使标记为持久化也不会迁移。 + +2. **组件数据完整保留**:迁移时所有组件及其状态都会保留。 + +3. **场景引用会更新**:迁移后实体的 `scene` 属性会指向新场景。 + +4. **查询系统会更新**:迁移的实体会自动注册到新场景的查询系统中。 + +5. **延迟切换同样生效**:使用 `Core.loadScene()` 延迟切换时,持久化实体同样会迁移。 + +## 相关文档 + +- [场景管理](./scene.md) - 了解场景的基本使用 +- [SceneManager](./scene-manager.md) - 了解场景切换 +- [WorldManager](./world-manager.md) - 了解多世界管理 diff --git a/docs/guide/scene-manager.md b/docs/guide/scene-manager.md index 8c8e910a..401b3c0c 100644 --- a/docs/guide/scene-manager.md +++ b/docs/guide/scene-manager.md @@ -19,6 +19,7 @@ SceneManager 适合以下场景: - 自动管理 ECS 流式 API - 自动处理场景生命周期 - 集成在 Core 中,自动更新 +- 支持[持久化实体](./persistent-entity.md)跨场景迁移(v2.2.22+) ## 基本使用 @@ -672,4 +673,9 @@ setTimeout(() => { }, 3000); ``` -SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。如果你需要更高级的多世界隔离功能,请参考 [WorldManager](./world-manager.md) 文档。 +SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。 + +## 相关文档 + +- [持久化实体](./persistent-entity.md) - 了解如何让实体跨场景保持状态 +- [WorldManager](./world-manager.md) - 了解更高级的多世界隔离功能 diff --git a/docs/guide/scene.md b/docs/guide/scene.md index 2be6a2a1..d5197efb 100644 --- a/docs/guide/scene.md +++ b/docs/guide/scene.md @@ -657,5 +657,6 @@ world.setSceneActive('main', true); - 了解 [SceneManager](./scene-manager.md) - 适用于大多数游戏的简单场景管理 - 了解 [WorldManager](./world-manager.md) - 适用于需要多世界隔离的高级场景 +- 了解 [持久化实体](./persistent-entity.md) - 让实体跨场景保持状态(v2.2.22+) 场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。 diff --git a/packages/core/src/ECS/Core/EntityLifecyclePolicy.ts b/packages/core/src/ECS/Core/EntityLifecyclePolicy.ts new file mode 100644 index 00000000..29f4932f --- /dev/null +++ b/packages/core/src/ECS/Core/EntityLifecyclePolicy.ts @@ -0,0 +1,26 @@ +/** + * 实体生命周期策略 + * + * 定义实体在场景切换时的行为。 + * + * Entity lifecycle policy. + * Defines entity behavior during scene transitions. + */ +export const enum EEntityLifecyclePolicy { + /** + * 默认策略 - 随场景销毁 + * + * Default policy - destroyed with scene. + */ + SceneLocal = 0, + + /** + * 持久化策略 - 跨场景保留 + * + * 实体在场景切换时自动迁移到新场景。 + * + * Persistent policy - survives scene transitions. + * Entity is automatically migrated to the new scene. + */ + Persistent = 1 +} diff --git a/packages/core/src/ECS/Entity.ts b/packages/core/src/ECS/Entity.ts index 70728ead..65651abb 100644 --- a/packages/core/src/ECS/Entity.ts +++ b/packages/core/src/ECS/Entity.ts @@ -1,5 +1,6 @@ import { Component } from './Component'; import { ComponentRegistry, ComponentType } from './Core/ComponentStorage'; +import { EEntityLifecyclePolicy } from './Core/EntityLifecyclePolicy'; import { BitMask64Utils, BitMask64Data } from './Utils/BigIntCompatibility'; import { createLogger } from '../Utils/Logger'; import { getComponentInstanceTypeName, getComponentTypeName } from './Decorators'; @@ -118,6 +119,13 @@ export class Entity { */ private _componentCache: Component[] | null = null; + /** + * 生命周期策略 + * + * Lifecycle policy for scene transitions. + */ + private _lifecyclePolicy: EEntityLifecyclePolicy = EEntityLifecyclePolicy.SceneLocal; + /** * 构造函数 * @@ -129,6 +137,61 @@ export class Entity { this.id = id; } + /** + * 获取生命周期策略 + * + * Get lifecycle policy. + */ + public get lifecyclePolicy(): EEntityLifecyclePolicy { + return this._lifecyclePolicy; + } + + /** + * 检查实体是否为持久化实体 + * + * Check if entity is persistent (survives scene transitions). + */ + public get isPersistent(): boolean { + return this._lifecyclePolicy === EEntityLifecyclePolicy.Persistent; + } + + /** + * 设置实体为持久化(跨场景保留) + * + * 标记后的实体在场景切换时不会被销毁,会自动迁移到新场景。 + * + * Mark entity as persistent (survives scene transitions). + * Persistent entities are automatically migrated to the new scene. + * + * @returns this,支持链式调用 | Returns this for chaining + * + * @example + * ```typescript + * const player = scene.createEntity('Player') + * .setPersistent() + * .addComponent(new PlayerComponent()); + * ``` + */ + public setPersistent(): this { + this._lifecyclePolicy = EEntityLifecyclePolicy.Persistent; + return this; + } + + /** + * 设置实体为场景本地(随场景销毁) + * + * 将实体恢复为默认行为。 + * + * Mark entity as scene-local (destroyed with scene). + * Restores default behavior. + * + * @returns this,支持链式调用 | Returns this for chaining + */ + public setSceneLocal(): this { + this._lifecyclePolicy = EEntityLifecyclePolicy.SceneLocal; + return this; + } + /** * 获取销毁状态 * @returns 如果实体已被销毁则返回true diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index cc922c31..04f1f2b3 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -819,6 +819,81 @@ export class Scene implements IScene { return result; } + /** + * 查找所有持久化实体 + * + * Find all persistent entities in this scene. + * + * @returns 持久化实体数组 | Array of persistent entities + */ + public findPersistentEntities(): Entity[] { + return this.entities.buffer.filter(entity => entity.isPersistent); + } + + /** + * 提取持久化实体(从场景中分离但不销毁) + * + * 用于场景切换时收集需要迁移的实体。 + * + * Extract persistent entities (detach from scene without destroying). + * Used during scene transitions to collect entities for migration. + * + * @returns 被提取的持久化实体数组 | Array of extracted persistent entities + * + * @internal + */ + public extractPersistentEntities(): Entity[] { + const persistentEntities = this.findPersistentEntities(); + + for (const entity of persistentEntities) { + // 从实体列表移除 + this.entities.remove(entity); + + // 从查询系统移除 + this.querySystem.removeEntity(entity); + + // 清除场景引用(但保留组件数据) + entity.scene = null; + } + + return persistentEntities; + } + + /** + * 接收迁移的实体 + * + * 将从其他场景迁移来的实体添加到当前场景。 + * + * Receive migrated entities from another scene. + * + * @param entities 要接收的实体数组 | Entities to receive + * + * @internal + */ + public receiveMigratedEntities(entities: Entity[]): void { + for (const entity of entities) { + // 设置新场景引用 + entity.scene = this; + + // 添加到实体列表 + this.entities.add(entity); + + // 添加到查询系统 + this.querySystem.addEntity(entity); + + // 重新注册组件到新场景的存储 + for (const component of entity.components) { + this.componentStorageManager.addComponent(entity.id, component); + this.referenceTracker?.registerEntityScene(entity.id, this); + } + } + + // 清除系统缓存 + if (entities.length > 0) { + this.clearSystemEntityCaches(); + } + } + /** * 根据名称查找实体(别名方法) * diff --git a/packages/core/src/ECS/SceneManager.ts b/packages/core/src/ECS/SceneManager.ts index dd9953b7..d459bf78 100644 --- a/packages/core/src/ECS/SceneManager.ts +++ b/packages/core/src/ECS/SceneManager.ts @@ -1,4 +1,6 @@ import { IScene } from './IScene'; +import { Scene } from './Scene'; +import { Entity } from './Entity'; import { ECSFluentAPI, createECSAPI } from './Core/FluentAPI'; import { Time } from '../Utils/Time'; import { createLogger } from '../Utils/Logger'; @@ -79,6 +81,13 @@ export class SceneManager implements IService { */ private _performanceMonitor: PerformanceMonitor | null = null; + /** + * 待迁移的持久化实体 + * + * Pending persistent entities for migration. + */ + private _pendingPersistentEntities: Entity[] = []; + /** * 默认场景ID */ @@ -104,17 +113,33 @@ export class SceneManager implements IService { * 设置当前场景(立即切换) * * 会自动处理旧场景的结束和新场景的初始化。 + * 持久化实体会自动迁移到新场景。 * - * @param scene - 要设置的场景实例 - * @returns 返回设置的场景实例,便于链式调用 + * Set current scene (immediate transition). + * Automatically handles old scene cleanup and new scene initialization. + * Persistent entities are automatically migrated to the new scene. + * + * @param scene - 要设置的场景实例 | Scene instance to set + * @returns 返回设置的场景实例,便于链式调用 | Returns the scene for chaining * * @example * ```typescript * const gameScene = sceneManager.setScene(new GameScene()); - * console.log(gameScene.name); // 可以立即使用返回的场景 + * console.log(gameScene.name); * ``` */ public setScene(scene: T): T { + // 从当前场景提取持久化实体 + const currentScene = this.currentScene; + if (currentScene && currentScene instanceof Scene) { + this._pendingPersistentEntities = currentScene.extractPersistentEntities(); + if (this._pendingPersistentEntities.length > 0) { + this._logger.debug( + `Extracted ${this._pendingPersistentEntities.length} persistent entities for migration` + ); + } + } + // 移除旧场景 this._defaultWorld.removeAllScenes(); @@ -127,6 +152,15 @@ export class SceneManager implements IService { this._defaultWorld.createScene(SceneManager.DEFAULT_SCENE_ID, scene); this._defaultWorld.setSceneActive(SceneManager.DEFAULT_SCENE_ID, true); + // 迁移持久化实体到新场景 + if (this._pendingPersistentEntities.length > 0 && scene instanceof Scene) { + scene.receiveMigratedEntities(this._pendingPersistentEntities); + this._logger.debug( + `Migrated ${this._pendingPersistentEntities.length} persistent entities to new scene` + ); + this._pendingPersistentEntities = []; + } + // 重建ECS API if (scene.querySystem && scene.eventSystem) { this._ecsAPI = createECSAPI(scene, scene.querySystem, scene.eventSystem); diff --git a/packages/core/src/ECS/index.ts b/packages/core/src/ECS/index.ts index 2a41ba79..f48411ad 100644 --- a/packages/core/src/ECS/index.ts +++ b/packages/core/src/ECS/index.ts @@ -1,5 +1,6 @@ export { Entity } from './Entity'; export { Component } from './Component'; +export { EEntityLifecyclePolicy } from './Core/EntityLifecyclePolicy'; export { ECSEventType, EventPriority, EVENT_TYPES, EventTypeValidator } from './CoreEvents'; export * from './Systems'; export * from './Utils'; diff --git a/packages/core/tests/ECS/PersistentEntity.test.ts b/packages/core/tests/ECS/PersistentEntity.test.ts new file mode 100644 index 00000000..1985b902 --- /dev/null +++ b/packages/core/tests/ECS/PersistentEntity.test.ts @@ -0,0 +1,424 @@ +import { Entity } from '../../src/ECS/Entity'; +import { Component } from '../../src/ECS/Component'; +import { Scene } from '../../src/ECS/Scene'; +import { SceneManager } from '../../src/ECS/SceneManager'; +import { EEntityLifecyclePolicy } from '../../src/ECS/Core/EntityLifecyclePolicy'; + +// 测试组件 +class PositionComponent extends Component { + public x: number; + public y: number; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +class PlayerComponent extends Component { + public name: string; + public score: number; + + constructor(name: string = 'Player', score: number = 0) { + super(); + this.name = name; + this.score = score; + } +} + +class EnemyComponent extends Component { + public type: string; + + constructor(type: string = 'normal') { + super(); + this.type = type; + } +} + +// 测试场景 +class TestScene extends Scene { + public initializeCalled = false; + + override initialize(): void { + this.initializeCalled = true; + } +} + +describe('PersistentEntity - 持久化实体测试', () => { + describe('Entity.setPersistent', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene(); + }); + + test('默认实体应为 SceneLocal 策略', () => { + const entity = scene.createEntity('NormalEntity'); + + expect(entity.lifecyclePolicy).toBe(EEntityLifecyclePolicy.SceneLocal); + expect(entity.isPersistent).toBe(false); + }); + + test('setPersistent() 应标记实体为持久化', () => { + const entity = scene.createEntity('Player'); + entity.setPersistent(); + + expect(entity.lifecyclePolicy).toBe(EEntityLifecyclePolicy.Persistent); + expect(entity.isPersistent).toBe(true); + }); + + test('setPersistent() 应支持链式调用', () => { + const entity = scene.createEntity('Player').setPersistent(); + entity.addComponent(new PositionComponent(100, 200)); + + expect(entity.isPersistent).toBe(true); + expect(entity.hasComponent(PositionComponent)).toBe(true); + }); + + test('setSceneLocal() 应恢复为默认策略', () => { + const entity = scene.createEntity('Player'); + entity.setPersistent(); + expect(entity.isPersistent).toBe(true); + + entity.setSceneLocal(); + expect(entity.isPersistent).toBe(false); + expect(entity.lifecyclePolicy).toBe(EEntityLifecyclePolicy.SceneLocal); + }); + }); + + describe('Scene.findPersistentEntities', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene(); + }); + + test('应返回所有持久化实体', () => { + // 创建混合实体 + const player = scene.createEntity('Player').setPersistent(); + const enemy1 = scene.createEntity('Enemy1'); + const gameManager = scene.createEntity('GameManager').setPersistent(); + const enemy2 = scene.createEntity('Enemy2'); + + const persistentEntities = scene.findPersistentEntities(); + + expect(persistentEntities.length).toBe(2); + expect(persistentEntities).toContain(player); + expect(persistentEntities).toContain(gameManager); + expect(persistentEntities).not.toContain(enemy1); + expect(persistentEntities).not.toContain(enemy2); + }); + + test('没有持久化实体时应返回空数组', () => { + scene.createEntity('Enemy1'); + scene.createEntity('Enemy2'); + + const persistentEntities = scene.findPersistentEntities(); + + expect(persistentEntities).toEqual([]); + }); + }); + + describe('Scene.extractPersistentEntities', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene(); + }); + + test('应提取并从场景中移除持久化实体', () => { + const player = scene.createEntity('Player').setPersistent(); + player.addComponent(new PositionComponent(100, 200)); + + const enemy = scene.createEntity('Enemy'); + + expect(scene.entities.count).toBe(2); + + const extracted = scene.extractPersistentEntities(); + + expect(extracted.length).toBe(1); + expect(extracted[0]).toBe(player); + expect(scene.entities.count).toBe(1); + expect(scene.findEntity('Player')).toBeNull(); + expect(scene.findEntity('Enemy')).toBe(enemy); + }); + + test('提取后实体的 scene 引用应为 null', () => { + const player = scene.createEntity('Player').setPersistent(); + + const extracted = scene.extractPersistentEntities(); + + expect(extracted[0].scene).toBeNull(); + }); + + test('提取后实体的组件数据应保留', () => { + const player = scene.createEntity('Player').setPersistent(); + player.addComponent(new PositionComponent(100, 200)); + player.addComponent(new PlayerComponent('Hero', 999)); + + const extracted = scene.extractPersistentEntities(); + + // 组件数据应保留(虽然 scene 为 null,组件缓存仍有效) + expect(extracted[0].components.length).toBe(2); + }); + }); + + describe('Scene.receiveMigratedEntities', () => { + test('应将迁移的实体添加到新场景', () => { + const sourceScene = new Scene(); + const targetScene = new Scene(); + + // 在源场景创建持久化实体 + const player = sourceScene.createEntity('Player').setPersistent(); + player.addComponent(new PositionComponent(100, 200)); + player.addComponent(new PlayerComponent('Hero', 500)); + + // 提取并迁移 + const extracted = sourceScene.extractPersistentEntities(); + targetScene.receiveMigratedEntities(extracted); + + // 验证实体已迁移 + expect(targetScene.entities.count).toBe(1); + expect(targetScene.findEntity('Player')).toBe(player); + expect(player.scene).toBe(targetScene); + }); + + test('迁移后组件数据应完整保留', () => { + const sourceScene = new Scene(); + const targetScene = new Scene(); + + const player = sourceScene.createEntity('Player').setPersistent(); + player.addComponent(new PositionComponent(100, 200)); + player.addComponent(new PlayerComponent('Hero', 999)); + + const extracted = sourceScene.extractPersistentEntities(); + targetScene.receiveMigratedEntities(extracted); + + // 验证组件数据 + const migratedPlayer = targetScene.findEntity('Player')!; + const position = migratedPlayer.getComponent(PositionComponent); + const playerComp = migratedPlayer.getComponent(PlayerComponent); + + expect(position).not.toBeNull(); + expect(position!.x).toBe(100); + expect(position!.y).toBe(200); + expect(playerComp).not.toBeNull(); + expect(playerComp!.name).toBe('Hero'); + expect(playerComp!.score).toBe(999); + }); + + test('迁移后实体应能被查询系统找到', () => { + const sourceScene = new Scene(); + const targetScene = new Scene(); + + const player = sourceScene.createEntity('Player').setPersistent(); + player.addComponent(new PositionComponent(100, 200)); + + const extracted = sourceScene.extractPersistentEntities(); + targetScene.receiveMigratedEntities(extracted); + + // 通过查询系统查找 + const result = targetScene.queryAll(PositionComponent); + expect(result.entities.length).toBe(1); + expect(result.entities[0]).toBe(player); + }); + }); + + describe('SceneManager 场景切换迁移', () => { + let sceneManager: SceneManager; + + beforeEach(() => { + sceneManager = new SceneManager(); + }); + + afterEach(() => { + sceneManager.destroy(); + }); + + test('场景切换时应自动迁移持久化实体', () => { + // 设置初始场景 + const scene1 = new TestScene(); + sceneManager.setScene(scene1); + + // 创建持久化实体和普通实体 + const player = scene1.createEntity('Player').setPersistent(); + player.addComponent(new PositionComponent(100, 200)); + player.addComponent(new PlayerComponent('Hero', 500)); + + const enemy = scene1.createEntity('Enemy'); + enemy.addComponent(new EnemyComponent('boss')); + + expect(scene1.entities.count).toBe(2); + + // 切换到新场景 + const scene2 = new TestScene(); + sceneManager.setScene(scene2); + + // 验证:player 应迁移到新场景,enemy 应被销毁 + expect(scene2.entities.count).toBe(1); + expect(scene2.findEntity('Player')).toBe(player); + expect(scene2.findEntity('Enemy')).toBeNull(); + expect(player.scene).toBe(scene2); + }); + + test('迁移后组件状态应保持不变', () => { + const scene1 = new TestScene(); + sceneManager.setScene(scene1); + + const player = scene1.createEntity('Player').setPersistent(); + player.addComponent(new PositionComponent(100, 200)); + const playerComp = player.addComponent(new PlayerComponent('Hero', 500)); + + // 修改组件状态 + playerComp.score = 999; + + // 切换场景 + const scene2 = new TestScene(); + sceneManager.setScene(scene2); + + // 验证组件状态 + const migratedPlayer = scene2.findEntity('Player')!; + const position = migratedPlayer.getComponent(PositionComponent); + const migratedPlayerComp = migratedPlayer.getComponent(PlayerComponent); + + expect(position!.x).toBe(100); + expect(position!.y).toBe(200); + expect(migratedPlayerComp!.score).toBe(999); + }); + + test('多个持久化实体应全部迁移', () => { + const scene1 = new TestScene(); + sceneManager.setScene(scene1); + + const player = scene1.createEntity('Player').setPersistent(); + const audioManager = scene1.createEntity('AudioManager').setPersistent(); + const gameState = scene1.createEntity('GameState').setPersistent(); + const enemy = scene1.createEntity('Enemy'); // 普通实体 + + expect(scene1.entities.count).toBe(4); + + const scene2 = new TestScene(); + sceneManager.setScene(scene2); + + expect(scene2.entities.count).toBe(3); + expect(scene2.findEntity('Player')).toBe(player); + expect(scene2.findEntity('AudioManager')).toBe(audioManager); + expect(scene2.findEntity('GameState')).toBe(gameState); + expect(scene2.findEntity('Enemy')).toBeNull(); + }); + + test('没有持久化实体时场景切换应正常工作', () => { + const scene1 = new TestScene(); + sceneManager.setScene(scene1); + + scene1.createEntity('Enemy1'); + scene1.createEntity('Enemy2'); + + const scene2 = new TestScene(); + sceneManager.setScene(scene2); + + expect(scene2.entities.count).toBe(0); + }); + + test('延迟场景切换应正确迁移持久化实体', () => { + const scene1 = new TestScene(); + sceneManager.setScene(scene1); + + const player = scene1.createEntity('Player').setPersistent(); + player.addComponent(new PlayerComponent('Hero', 100)); + + // 延迟加载 + const scene2 = new TestScene(); + sceneManager.loadScene(scene2); + + // 此时还未切换 + expect(sceneManager.currentScene).toBe(scene1); + expect(scene1.findEntity('Player')).toBe(player); + + // 触发更新,执行延迟切换 + sceneManager.update(); + + // 验证迁移 + expect(sceneManager.currentScene).toBe(scene2); + expect(scene2.findEntity('Player')).toBe(player); + expect(player.scene).toBe(scene2); + }); + + test('连续场景切换应正确迁移持久化实体', () => { + const scene1 = new TestScene(); + sceneManager.setScene(scene1); + + const player = scene1.createEntity('Player').setPersistent(); + + // 第一次切换 + const scene2 = new TestScene(); + sceneManager.setScene(scene2); + expect(scene2.findEntity('Player')).toBe(player); + + // 第二次切换 + const scene3 = new TestScene(); + sceneManager.setScene(scene3); + expect(scene3.findEntity('Player')).toBe(player); + + // 第三次切换 + const scene4 = new TestScene(); + sceneManager.setScene(scene4); + expect(scene4.findEntity('Player')).toBe(player); + expect(player.scene).toBe(scene4); + }); + }); + + describe('边界情况', () => { + test('实体销毁后不应被迁移', () => { + const sceneManager = new SceneManager(); + const scene1 = new TestScene(); + sceneManager.setScene(scene1); + + const player = scene1.createEntity('Player').setPersistent(); + player.destroy(); + + const scene2 = new TestScene(); + sceneManager.setScene(scene2); + + expect(scene2.entities.count).toBe(0); + sceneManager.destroy(); + }); + + test('动态切换持久化状态应生效', () => { + const sceneManager = new SceneManager(); + const scene1 = new TestScene(); + sceneManager.setScene(scene1); + + const entity = scene1.createEntity('DynamicEntity'); + expect(entity.isPersistent).toBe(false); + + // 动态设为持久化 + entity.setPersistent(); + expect(entity.isPersistent).toBe(true); + + const scene2 = new TestScene(); + sceneManager.setScene(scene2); + + expect(scene2.findEntity('DynamicEntity')).toBe(entity); + sceneManager.destroy(); + }); + + test('动态取消持久化状态应生效', () => { + const sceneManager = new SceneManager(); + const scene1 = new TestScene(); + sceneManager.setScene(scene1); + + const entity = scene1.createEntity('DynamicEntity').setPersistent(); + + // 动态取消持久化 + entity.setSceneLocal(); + + const scene2 = new TestScene(); + sceneManager.setScene(scene2); + + expect(scene2.findEntity('DynamicEntity')).toBeNull(); + sceneManager.destroy(); + }); + }); +});