diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 0cee0f1b..4f834714 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -80,6 +80,14 @@ export default defineConfig({ { text: '日志系统 (Logger)', link: '/guide/logging' } ] }, + { + text: '高级特性', + collapsed: false, + items: [ + { text: '服务容器 (Service Container)', link: '/guide/service-container' }, + { text: '插件系统 (Plugin System)', link: '/guide/plugin-system' } + ] + }, { text: '平台适配器', link: '/guide/platform-adapter', diff --git a/docs/guide/component.md b/docs/guide/component.md index 3a74edd5..f4d41641 100644 --- a/docs/guide/component.md +++ b/docs/guide/component.md @@ -63,14 +63,14 @@ class Health extends Component { - 框架能正确管理组件注册 ```typescript -// ✅ 正确的用法 +// 正确的用法 @ECSComponent('Velocity') class Velocity extends Component { dx: number = 0; dy: number = 0; } -// ❌ 错误的用法 - 没有装饰器 +// 错误的用法 - 没有装饰器 class BadComponent extends Component { // 这样定义的组件可能在生产环境出现问题 } @@ -90,7 +90,7 @@ class ExampleComponent extends Component { * 用于初始化资源、建立引用等 */ onAddedToEntity(): void { - console.log(`组件 ${this.constructor.name} 被添加到实体 ${this.entity.name}`); + console.log(`组件 ${this.constructor.name} 已添加,实体ID: ${this.entityId}`); this.resource = new SomeResource(); } @@ -99,7 +99,7 @@ class ExampleComponent extends Component { * 用于清理资源、断开引用等 */ onRemovedFromEntity(): void { - console.log(`组件 ${this.constructor.name} 从实体 ${this.entity.name} 移除`); + console.log(`组件 ${this.constructor.name} 已移除`); if (this.resource) { this.resource.cleanup(); this.resource = null; @@ -108,30 +108,58 @@ class ExampleComponent extends Component { } ``` -## 访问实体 +## 组件与实体的关系 -组件可以通过 `this.entity` 访问其所属的实体: +组件存储了所属实体的ID (`entityId`),而不是直接引用实体对象。这是ECS数据导向设计的体现,避免了循环引用。 + +在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部: ```typescript -@ECSComponent('Damage') -class Damage extends Component { - damage: number; +@ECSComponent('Health') +class Health extends Component { + current: number; + max: number; - constructor(damage: number) { + constructor(max: number = 100) { super(); - this.damage = damage; + this.max = max; + this.current = max; } - // 在组件方法中访问实体和其他组件 - applyDamage(): void { - const health = this.entity.getComponent(Health); - if (health) { - health.takeDamage(this.damage); + isDead(): boolean { + return this.current <= 0; + } +} + +@ECSComponent('Damage') +class Damage extends Component { + value: number; + + constructor(value: number) { + super(); + this.value = value; + } +} + +// 推荐:在 System 中处理逻辑 +class DamageSystem extends EntitySystem { + constructor() { + super(new Matcher().all(Health, Damage)); + } + + process(entities: readonly Entity[]): void { + for (const entity of entities) { + const health = entity.getComponent(Health)!; + const damage = entity.getComponent(Damage)!; + + health.current -= damage.value; - // 如果生命值为0,销毁实体 if (health.isDead()) { - this.entity.destroy(); + entity.destroy(); } + + // 应用伤害后移除 Damage 组件 + entity.removeComponent(damage); } } } @@ -146,9 +174,27 @@ class Damage extends Component { class ExampleComponent extends Component { someData: string = "example"; - showComponentInfo(): void { - console.log(`组件ID: ${this.id}`); // 唯一的组件ID - console.log(`所属实体: ${this.entity.name}`); // 所属实体引用 + onAddedToEntity(): void { + console.log(`组件ID: ${this.id}`); // 唯一的组件ID + console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID + } +} +``` + +如果需要访问实体对象,应该在 System 中进行: + +```typescript +class ExampleSystem extends EntitySystem { + constructor() { + super(new Matcher().all(ExampleComponent)); + } + + process(entities: readonly Entity[]): void { + for (const entity of entities) { + const comp = entity.getComponent(ExampleComponent)!; + console.log(`实体名称: ${entity.name}`); + console.log(`组件数据: ${comp.someData}`); + } } } ``` @@ -245,7 +291,7 @@ class WeaponConfig extends Component { ### 1. 保持组件简单 ```typescript -// ✅ 好的组件设计 - 单一职责 +// 好的组件设计 - 单一职责 @ECSComponent('Position') class Position extends Component { x: number = 0; @@ -258,7 +304,7 @@ class Velocity extends Component { dy: number = 0; } -// ❌ 避免的组件设计 - 职责过多 +// 避免的组件设计 - 职责过多 @ECSComponent('GameObject') class GameObject extends Component { x: number; @@ -330,16 +376,11 @@ class Inventory extends Component { } ``` -### 4. 避免在组件中存储实体引用 +### 4. 引用其他实体 + +当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找: ```typescript -// ❌ 避免:在组件中存储其他实体的引用 -@ECSComponent('BadFollower') -class BadFollower extends Component { - target: Entity; // 直接引用可能导致内存泄漏 -} - -// ✅ 推荐:存储实体ID,通过场景查找 @ECSComponent('Follower') class Follower extends Component { targetId: number; @@ -349,11 +390,269 @@ class Follower extends Component { super(); this.targetId = targetId; } +} - getTarget(): Entity | null { - return this.entity.scene?.findEntityById(this.targetId) || null; +// 在 System 中查找目标实体并处理逻辑 +class FollowerSystem extends EntitySystem { + constructor() { + super(new Matcher().all(Follower, Position)); + } + + process(entities: readonly Entity[]): void { + for (const entity of entities) { + const follower = entity.getComponent(Follower)!; + const position = entity.getComponent(Position)!; + + // 通过场景查找目标实体 + const target = entity.scene?.findEntityById(follower.targetId); + if (target) { + const targetPos = target.getComponent(Position); + if (targetPos) { + // 跟随逻辑 + const dx = targetPos.x - position.x; + const dy = targetPos.y - position.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > follower.followDistance) { + // 移动靠近目标 + } + } + } + } } } ``` +这种方式的优势: +- 组件保持简单,只存储基本数据类型 +- 符合数据导向设计 +- 在 System 中统一处理查找和逻辑 +- 易于理解和维护 + +**避免在组件中直接存储实体引用**: + +```typescript +// 错误示范:直接存储实体引用 +@ECSComponent('BadFollower') +class BadFollower extends Component { + target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏 +} +``` + +## 高级特性 + +### EntityRef 装饰器 - 自动引用追踪 + +框架提供了 `@EntityRef` 装饰器用于**特殊场景**下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。 + +#### 什么时候需要 EntityRef? + +在以下场景中,`@EntityRef` 可以简化代码: + +1. **父子关系**: 需要在组件中直接访问父实体或子实体 +2. **复杂关联**: 实体之间有多个引用关系 +3. **频繁访问**: 需要在多处访问引用的实体,使用ID查找会有性能开销 + +#### 核心特性 + +`@EntityRef` 装饰器通过 **ReferenceTracker** 自动追踪引用关系: + +- 当被引用的实体销毁时,所有指向它的 `@EntityRef` 属性自动设为 `null` +- 防止跨场景引用(会输出警告并拒绝设置) +- 防止引用已销毁的实体(会输出警告并设为 `null`) +- 使用 WeakRef 避免内存泄漏(自动GC支持) +- 组件移除时自动清理引用注册 + +#### 基本用法 + +```typescript +import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework'; + +@ECSComponent('Parent') +class ParentComponent extends Component { + @EntityRef() + parent: Entity | null = null; +} + +// 使用示例 +const scene = new Scene(); +const parent = scene.createEntity('Parent'); +const child = scene.createEntity('Child'); + +const comp = child.addComponent(new ParentComponent()); +comp.parent = parent; + +console.log(comp.parent); // Entity { name: 'Parent' } + +// 当 parent 被销毁时,comp.parent 自动变为 null +parent.destroy(); +console.log(comp.parent); // null +``` + +#### 多个引用属性 + +一个组件可以有多个 `@EntityRef` 属性: + +```typescript +@ECSComponent('Combat') +class CombatComponent extends Component { + @EntityRef() + target: Entity | null = null; + + @EntityRef() + ally: Entity | null = null; + + @EntityRef() + lastAttacker: Entity | null = null; +} + +// 使用示例 +const player = scene.createEntity('Player'); +const enemy = scene.createEntity('Enemy'); +const npc = scene.createEntity('NPC'); + +const combat = player.addComponent(new CombatComponent()); +combat.target = enemy; +combat.ally = npc; + +// enemy 销毁后,只有 target 变为 null,ally 仍然有效 +enemy.destroy(); +console.log(combat.target); // null +console.log(combat.ally); // Entity { name: 'NPC' } +``` + +#### 安全检查 + +`@EntityRef` 提供了多重安全检查: + +```typescript +const scene1 = new Scene(); +const scene2 = new Scene(); + +const entity1 = scene1.createEntity('Entity1'); +const entity2 = scene2.createEntity('Entity2'); + +const comp = entity1.addComponent(new ParentComponent()); + +// 跨场景引用会失败 +comp.parent = entity2; // 输出错误日志,comp.parent 为 null +console.log(comp.parent); // null + +// 引用已销毁的实体会失败 +const entity3 = scene1.createEntity('Entity3'); +entity3.destroy(); +comp.parent = entity3; // 输出警告日志,comp.parent 为 null +console.log(comp.parent); // null +``` + +#### 实现原理 + +`@EntityRef` 使用以下机制实现自动引用追踪: + +1. **ReferenceTracker**: Scene 持有一个引用追踪器,记录所有实体引用关系 +2. **WeakRef**: 使用弱引用存储组件,避免循环引用导致内存泄漏 +3. **属性拦截**: 通过 `Object.defineProperty` 拦截 getter/setter +4. **自动清理**: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null + +```typescript +// 简化的实现原理 +class ReferenceTracker { + // entityId -> 引用该实体的所有组件记录 + private _references: Map, propertyKey: string }>>; + + // 实体销毁时调用 + clearReferencesTo(entityId: number): void { + const records = this._references.get(entityId); + if (records) { + for (const record of records) { + const component = record.component.deref(); + if (component) { + // 将组件的引用属性设为 null + (component as any)[record.propertyKey] = null; + } + } + this._references.delete(entityId); + } + } +} +``` + +#### 性能考虑 + +`@EntityRef` 会带来一些性能开销: + +- **写入开销**: 每次设置引用时需要更新 ReferenceTracker +- **内存开销**: ReferenceTracker 需要维护引用映射表 +- **销毁开销**: 实体销毁时需要遍历所有引用并清理 + +对于大多数场景,这些开销是可以接受的。但如果有**大量实体和频繁的引用变更**,存储ID可能更高效。 + +#### 最佳实践 + +```typescript +// 推荐:适合使用 @EntityRef 的场景 - 父子关系 +@ECSComponent('Transform') +class Transform extends Component { + @EntityRef() + parent: Entity | null = null; + + position: { x: number, y: number } = { x: 0, y: 0 }; + + // 可以直接访问父实体的组件 + getWorldPosition(): { x: number, y: number } { + if (!this.parent) { + return { ...this.position }; + } + + const parentTransform = this.parent.getComponent(Transform); + if (parentTransform) { + const parentPos = parentTransform.getWorldPosition(); + return { + x: parentPos.x + this.position.x, + y: parentPos.y + this.position.y + }; + } + + return { ...this.position }; + } +} + +// 不推荐:不适合使用 @EntityRef 的场景 - 大量动态目标 +@ECSComponent('AITarget') +class AITarget extends Component { + @EntityRef() + target: Entity | null = null; // 如果目标频繁变化,用ID更好 + + updateCooldown: number = 0; +} + +// 推荐:这种场景用ID更好 +@ECSComponent('AITarget') +class AITargetBetter extends Component { + targetId: number | null = null; // 存储ID + updateCooldown: number = 0; +} +``` + +#### 调试支持 + +ReferenceTracker 提供了调试接口: + +```typescript +// 查看某个实体被哪些组件引用 +const references = scene.referenceTracker.getReferencesTo(entity.id); +console.log(`实体 ${entity.name} 被 ${references.length} 个组件引用`); + +// 获取完整的调试信息 +const debugInfo = scene.referenceTracker.getDebugInfo(); +console.log(debugInfo); +``` + +#### 总结 + +- **推荐做法**: 大部分情况使用存储ID + System查找的方式 +- **EntityRef 适用场景**: 父子关系、复杂关联、组件内需要直接访问引用实体的场景 +- **核心优势**: 自动清理、防止悬空引用、代码更简洁 +- **注意事项**: 有性能开销,不适合大量动态引用的场景 + 组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。 \ No newline at end of file diff --git a/docs/guide/index.md b/docs/guide/index.md index bb49bd2f..8009b745 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -29,4 +29,12 @@ 掌握分级日志系统,用于调试、监控和错误追踪。 ### [平台适配器 (Platform Adapter)](./platform-adapter.md) -了解如何为不同平台实现和注册平台适配器,支持浏览器、小游戏、Node.js等环境。 \ No newline at end of file +了解如何为不同平台实现和注册平台适配器,支持浏览器、小游戏、Node.js等环境。 + +## 高级特性 + +### [服务容器 (Service Container)](./service-container.md) +掌握依赖注入和服务管理,实现松耦合的架构设计。 + +### [插件系统 (Plugin System)](./plugin-system.md) +学习如何开发和使用插件,扩展框架功能,实现功能模块化。 \ No newline at end of file diff --git a/docs/guide/plugin-system.md b/docs/guide/plugin-system.md new file mode 100644 index 00000000..af4803e1 --- /dev/null +++ b/docs/guide/plugin-system.md @@ -0,0 +1,643 @@ +# 插件系统 + +插件系统允许你以模块化的方式扩展 ECS Framework 的功能。通过插件,你可以封装特定功能(如网络同步、物理引擎、调试工具等),并在多个项目中复用。 + +## 概述 + +### 什么是插件 + +插件是实现了 `IPlugin` 接口的类,可以在运行时动态安装到框架中。插件可以: + +- 注册自定义服务到服务容器 +- 添加系统到场景 +- 注册自定义组件 +- 扩展框架功能 + +### 插件的优势 + +- **模块化**: 将功能封装为独立模块,提高代码可维护性 +- **可复用**: 同一个插件可以在多个项目中使用 +- **解耦**: 核心框架与扩展功能分离 +- **热插拔**: 运行时动态安装和卸载插件 + +## 快速开始 + +### 创建第一个插件 + +创建一个简单的调试插件: + +```typescript +import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework'; + +class DebugPlugin implements IPlugin { + readonly name = 'debug-plugin'; + readonly version = '1.0.0'; + + install(core: Core, services: ServiceContainer): void { + console.log('Debug plugin installed'); + + // 可以在这里注册服务、添加系统等 + } + + uninstall(): void { + console.log('Debug plugin uninstalled'); + // 清理资源 + } +} +``` + +### 安装插件 + +使用 `Core.installPlugin()` 安装插件: + +```typescript +import { Core } from '@esengine/ecs-framework'; + +// 初始化Core +Core.create({ debug: true }); + +// 安装插件 +await Core.installPlugin(new DebugPlugin()); + +// 检查插件是否已安装 +if (Core.isPluginInstalled('debug-plugin')) { + console.log('Debug plugin is running'); +} +``` + +### 卸载插件 + +```typescript +// 卸载插件 +await Core.uninstallPlugin('debug-plugin'); +``` + +### 获取插件实例 + +```typescript +// 获取已安装的插件 +const plugin = Core.getPlugin('debug-plugin'); +if (plugin) { + console.log(`Plugin version: ${plugin.version}`); +} +``` + +## 插件开发 + +### IPlugin 接口 + +所有插件必须实现 `IPlugin` 接口: + +```typescript +export interface IPlugin { + // 插件唯一名称 + readonly name: string; + + // 插件版本(建议遵循semver规范) + readonly version: string; + + // 依赖的其他插件(可选) + readonly dependencies?: readonly string[]; + + // 安装插件时调用 + install(core: Core, services: ServiceContainer): void | Promise; + + // 卸载插件时调用 + uninstall(): void | Promise; +} +``` + +### 插件生命周期 + +#### install 方法 + +在插件安装时调用,用于初始化插件: + +```typescript +class MyPlugin implements IPlugin { + readonly name = 'my-plugin'; + readonly version = '1.0.0'; + + install(core: Core, services: ServiceContainer): void { + // 1. 注册服务 + services.registerSingleton(MyService); + + // 2. 访问当前场景 + const scene = core.scene; + if (scene) { + // 3. 添加系统 + scene.addSystem(new MySystem()); + } + + // 4. 其他初始化逻辑 + console.log('Plugin initialized'); + } + + uninstall(): void { + // 清理逻辑 + } +} +``` + +#### uninstall 方法 + +在插件卸载时调用,用于清理资源: + +```typescript +class MyPlugin implements IPlugin { + readonly name = 'my-plugin'; + readonly version = '1.0.0'; + private myService?: MyService; + + install(core: Core, services: ServiceContainer): void { + this.myService = new MyService(); + services.registerInstance(MyService, this.myService); + } + + uninstall(): void { + // 清理服务 + if (this.myService) { + this.myService.dispose(); + this.myService = undefined; + } + + // 移除事件监听器 + // 释放其他资源 + } +} +``` + +### 异步插件 + +插件的 `install` 和 `uninstall` 方法都支持异步: + +```typescript +class AsyncPlugin implements IPlugin { + readonly name = 'async-plugin'; + readonly version = '1.0.0'; + + async install(core: Core, services: ServiceContainer): Promise { + // 异步加载资源 + const config = await fetch('/plugin-config.json').then(r => r.json()); + + // 使用加载的配置初始化服务 + const service = new MyService(config); + services.registerInstance(MyService, service); + } + + async uninstall(): Promise { + // 异步清理 + await this.saveState(); + } + + private async saveState() { + // 保存插件状态 + } +} + +// 使用 +await Core.installPlugin(new AsyncPlugin()); +``` + +### 注册服务 + +插件可以向服务容器注册自己的服务: + +```typescript +import { IService } from '@esengine/ecs-framework'; + +class NetworkService implements IService { + connect(url: string) { + console.log(`Connecting to ${url}`); + } + + dispose(): void { + console.log('Network service disposed'); + } +} + +class NetworkPlugin implements IPlugin { + readonly name = 'network-plugin'; + readonly version = '1.0.0'; + + install(core: Core, services: ServiceContainer): void { + // 注册网络服务 + services.registerSingleton(NetworkService); + + // 解析并使用服务 + const network = services.resolve(NetworkService); + network.connect('ws://localhost:8080'); + } + + uninstall(): void { + // 服务容器会自动调用服务的dispose方法 + } +} +``` + +### 添加系统 + +插件可以向场景添加自定义系统: + +```typescript +import { EntitySystem, Matcher } from '@esengine/ecs-framework'; + +class PhysicsSystem extends EntitySystem { + constructor() { + super(Matcher.empty().all(PhysicsBody)); + } + + protected process(entities: readonly Entity[]): void { + // 物理模拟逻辑 + } +} + +class PhysicsPlugin implements IPlugin { + readonly name = 'physics-plugin'; + readonly version = '1.0.0'; + private physicsSystem?: PhysicsSystem; + + install(core: Core, services: ServiceContainer): void { + const scene = core.scene; + if (scene) { + this.physicsSystem = new PhysicsSystem(); + scene.addSystem(this.physicsSystem); + } + } + + uninstall(): void { + // 移除系统 + if (this.physicsSystem) { + const scene = Core.scene; + if (scene) { + scene.removeSystem(this.physicsSystem); + } + this.physicsSystem = undefined; + } + } +} +``` + +## 依赖管理 + +### 声明依赖 + +插件可以声明对其他插件的依赖: + +```typescript +class AdvancedPhysicsPlugin implements IPlugin { + readonly name = 'advanced-physics'; + readonly version = '2.0.0'; + + // 声明依赖基础物理插件 + readonly dependencies = ['physics-plugin'] as const; + + install(core: Core, services: ServiceContainer): void { + // 可以安全地使用physics-plugin提供的服务 + const physicsService = services.resolve(PhysicsService); + // ... + } + + uninstall(): void { + // 清理 + } +} +``` + +### 依赖检查 + +框架会自动检查依赖关系,如果依赖未满足会抛出错误: + +```typescript +// 错误:physics-plugin 未安装 +try { + await Core.installPlugin(new AdvancedPhysicsPlugin()); +} catch (error) { + console.error(error); // Plugin advanced-physics has unmet dependencies: physics-plugin +} + +// 正确:先安装依赖 +await Core.installPlugin(new PhysicsPlugin()); +await Core.installPlugin(new AdvancedPhysicsPlugin()); +``` + +### 卸载顺序 + +框架会检查依赖关系,防止卸载被其他插件依赖的插件: + +```typescript +await Core.installPlugin(new PhysicsPlugin()); +await Core.installPlugin(new AdvancedPhysicsPlugin()); + +// 错误:physics-plugin 被 advanced-physics 依赖 +try { + await Core.uninstallPlugin('physics-plugin'); +} catch (error) { + console.error(error); // Cannot uninstall plugin physics-plugin: it is required by advanced-physics +} + +// 正确:先卸载依赖它的插件 +await Core.uninstallPlugin('advanced-physics'); +await Core.uninstallPlugin('physics-plugin'); +``` + +## 插件管理 + +### 通过 Core 管理 + +Core 类提供了便捷的插件管理方法: + +```typescript +// 安装插件 +await Core.installPlugin(myPlugin); + +// 卸载插件 +await Core.uninstallPlugin('plugin-name'); + +// 检查插件是否已安装 +if (Core.isPluginInstalled('plugin-name')) { + // ... +} + +// 获取插件实例 +const plugin = Core.getPlugin('plugin-name'); +``` + +### 通过 PluginManager 管理 + +也可以直接使用 PluginManager 服务: + +```typescript +const pluginManager = Core.services.resolve(PluginManager); + +// 获取所有插件 +const allPlugins = pluginManager.getAllPlugins(); +console.log(`Total plugins: ${allPlugins.length}`); + +// 获取插件元数据 +const metadata = pluginManager.getMetadata('my-plugin'); +if (metadata) { + console.log(`State: ${metadata.state}`); + console.log(`Installed at: ${new Date(metadata.installedAt!)}`); +} + +// 获取所有插件元数据 +const allMetadata = pluginManager.getAllMetadata(); +for (const meta of allMetadata) { + console.log(`${meta.name} v${meta.version} - ${meta.state}`); +} +``` + +## 实用插件示例 + +### 网络同步插件 + +```typescript +import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework'; + +class NetworkSyncService implements IService { + private ws?: WebSocket; + + connect(url: string) { + this.ws = new WebSocket(url); + this.ws.onmessage = (event) => { + const data = JSON.parse(event.data); + this.handleMessage(data); + }; + } + + private handleMessage(data: any) { + // 处理网络消息 + } + + dispose(): void { + if (this.ws) { + this.ws.close(); + this.ws = undefined; + } + } +} + +class NetworkSyncPlugin implements IPlugin { + readonly name = 'network-sync'; + readonly version = '1.0.0'; + + install(core: Core, services: ServiceContainer): void { + // 注册网络服务 + services.registerSingleton(NetworkSyncService); + + // 自动连接 + const network = services.resolve(NetworkSyncService); + network.connect('ws://localhost:8080'); + } + + uninstall(): void { + // 服务会自动dispose + } +} +``` + +### 性能分析插件 + +```typescript +class PerformanceAnalysisPlugin implements IPlugin { + readonly name = 'performance-analysis'; + readonly version = '1.0.0'; + private frameCount = 0; + private totalTime = 0; + + install(core: Core, services: ServiceContainer): void { + const monitor = services.resolve(PerformanceMonitor); + monitor.enable(); + + // 定期输出性能报告 + const timer = services.resolve(TimerManager); + timer.schedule(5.0, true, null, () => { + this.printReport(monitor); + }); + } + + uninstall(): void { + // 清理 + } + + private printReport(monitor: PerformanceMonitor) { + console.log('=== Performance Report ==='); + console.log(`FPS: ${monitor.getFPS()}`); + console.log(`Memory: ${monitor.getMemoryUsage()} MB`); + } +} +``` + +## 最佳实践 + +### 命名规范 + +- 插件名称使用小写字母和连字符:`my-awesome-plugin` +- 版本号遵循语义化版本规范:`1.0.0` + +```typescript +class MyPlugin implements IPlugin { + readonly name = 'my-awesome-plugin'; // 好 + readonly version = '1.0.0'; // 好 +} +``` + +### 清理资源 + +始终在 `uninstall` 中清理插件创建的所有资源: + +```typescript +class MyPlugin implements IPlugin { + readonly name = 'my-plugin'; + readonly version = '1.0.0'; + private timerId?: number; + private listener?: () => void; + + install(core: Core, services: ServiceContainer): void { + // 添加定时器 + this.timerId = setInterval(() => { + // ... + }, 1000); + + // 添加事件监听 + this.listener = () => {}; + window.addEventListener('resize', this.listener); + } + + uninstall(): void { + // 清理定时器 + if (this.timerId) { + clearInterval(this.timerId); + this.timerId = undefined; + } + + // 移除事件监听 + if (this.listener) { + window.removeEventListener('resize', this.listener); + this.listener = undefined; + } + } +} +``` + +### 错误处理 + +在插件中妥善处理错误,避免影响整个应用: + +```typescript +class MyPlugin implements IPlugin { + readonly name = 'my-plugin'; + readonly version = '1.0.0'; + + async install(core: Core, services: ServiceContainer): Promise { + try { + // 可能失败的操作 + await this.loadConfig(); + } catch (error) { + console.error('Failed to load plugin config:', error); + throw error; // 重新抛出,让框架知道安装失败 + } + } + + async uninstall(): Promise { + try { + await this.cleanup(); + } catch (error) { + console.error('Failed to cleanup plugin:', error); + // 即使清理失败也不应该阻止卸载 + } + } + + private async loadConfig() { + // 加载配置 + } + + private async cleanup() { + // 清理 + } +} +``` + +### 配置化 + +允许用户配置插件行为: + +```typescript +interface NetworkPluginConfig { + serverUrl: string; + autoReconnect: boolean; + timeout: number; +} + +class NetworkPlugin implements IPlugin { + readonly name = 'network-plugin'; + readonly version = '1.0.0'; + + constructor(private config: NetworkPluginConfig) {} + + install(core: Core, services: ServiceContainer): void { + const network = new NetworkService(this.config); + services.registerInstance(NetworkService, network); + } + + uninstall(): void { + // 清理 + } +} + +// 使用 +const plugin = new NetworkPlugin({ + serverUrl: 'ws://localhost:8080', + autoReconnect: true, + timeout: 5000 +}); + +await Core.installPlugin(plugin); +``` + +## 常见问题 + +### 插件安装失败 + +**问题**: 插件安装时抛出错误 + +**原因**: +- 依赖未满足 +- install 方法中有异常 +- 服务注册冲突 + +**解决**: +1. 检查依赖是否已安装 +2. 查看错误日志 +3. 确保服务名称不冲突 + +### 插件卸载后仍有副作用 + +**问题**: 卸载插件后,插件的功能仍在运行 + +**原因**: uninstall 方法中未正确清理资源 + +**解决**: 确保在 uninstall 中清理: +- 定时器 +- 事件监听器 +- WebSocket连接 +- 系统引用 + +### 何时使用插件 + +**适合使用插件**: +- 可选功能(调试工具、性能分析) +- 第三方集成(网络库、物理引擎) +- 跨项目复用的功能模块 + +**不适合使用插件**: +- 核心游戏逻辑 +- 简单的工具类 +- 项目特定的功能 + +## 相关链接 + +- [服务容器](./service-container.md) - 在插件中使用服务容器 +- [系统架构](./system.md) - 在插件中添加系统 +- [快速开始](./getting-started.md) - Core 初始化和基础使用 diff --git a/docs/guide/service-container.md b/docs/guide/service-container.md new file mode 100644 index 00000000..8e9675e3 --- /dev/null +++ b/docs/guide/service-container.md @@ -0,0 +1,555 @@ +# 服务容器 + +服务容器(ServiceContainer)是 ECS Framework 的依赖注入容器,负责管理框架中所有服务的注册、解析和生命周期。通过服务容器,你可以实现松耦合的架构设计,提高代码的可测试性和可维护性。 + +## 概述 + +### 什么是服务容器 + +服务容器是一个轻量级的依赖注入(DI)容器,它提供了: + +- **服务注册**: 将服务类型注册到容器中 +- **服务解析**: 从容器中获取服务实例 +- **生命周期管理**: 自动管理服务实例的创建和销毁 +- **依赖注入**: 自动解析服务之间的依赖关系 + +### 核心概念 + +#### 服务(Service) + +服务是实现了 `IService` 接口的类,必须提供 `dispose()` 方法用于资源清理: + +```typescript +import { IService } from '@esengine/ecs-framework'; + +class MyService implements IService { + constructor() { + // 初始化逻辑 + } + + dispose(): void { + // 清理资源 + } +} +``` + +#### 生命周期 + +服务容器支持两种生命周期: + +- **Singleton(单例)**: 整个应用程序生命周期内只有一个实例,所有解析请求返回同一个实例 +- **Transient(瞬时)**: 每次解析都创建新的实例 + +## 基础使用 + +### 访问服务容器 + +Core 类内置了服务容器,可以通过 `Core.services` 访问: + +```typescript +import { Core } from '@esengine/ecs-framework'; + +// 初始化Core +Core.create({ debug: true }); + +// 访问服务容器 +const container = Core.services; +``` + +### 注册服务 + +#### 注册单例服务 + +单例服务在首次解析时创建,之后所有解析请求都返回同一个实例: + +```typescript +class DataService implements IService { + private data: Map = new Map(); + + getData(key: string) { + return this.data.get(key); + } + + setData(key: string, value: any) { + this.data.set(key, value); + } + + dispose(): void { + this.data.clear(); + } +} + +// 注册单例服务 +Core.services.registerSingleton(DataService); +``` + +#### 注册瞬时服务 + +瞬时服务每次解析都创建新实例,适用于无状态或短生命周期的服务: + +```typescript +class CommandService implements IService { + execute(command: string) { + console.log(`Executing: ${command}`); + } + + dispose(): void { + // 清理资源 + } +} + +// 注册瞬时服务 +Core.services.registerTransient(CommandService); +``` + +#### 注册服务实例 + +直接注册已创建的实例,自动视为单例: + +```typescript +const config = new ConfigService(); +config.load('./config.json'); + +// 注册实例 +Core.services.registerInstance(ConfigService, config); +``` + +#### 使用工厂函数注册 + +工厂函数允许你在创建服务时执行自定义逻辑: + +```typescript +Core.services.registerSingleton(LoggerService, (container) => { + const logger = new LoggerService(); + logger.setLevel('debug'); + return logger; +}); +``` + +### 解析服务 + +#### resolve 方法 + +解析服务实例,如果服务未注册会抛出异常: + +```typescript +// 解析服务 +const dataService = Core.services.resolve(DataService); +dataService.setData('player', { name: 'Alice', score: 100 }); + +// 单例服务,多次解析返回同一个实例 +const same = Core.services.resolve(DataService); +console.log(same === dataService); // true +``` + +#### tryResolve 方法 + +尝试解析服务,如果未注册返回 null 而不抛出异常: + +```typescript +const optional = Core.services.tryResolve(OptionalService); +if (optional) { + optional.doSomething(); +} +``` + +#### isRegistered 方法 + +检查服务是否已注册: + +```typescript +if (Core.services.isRegistered(DataService)) { + const service = Core.services.resolve(DataService); +} +``` + +## 内置服务 + +Core 在初始化时自动注册了以下内置服务: + +### TimerManager + +定时器管理器,负责管理所有游戏定时器: + +```typescript +const timerManager = Core.services.resolve(TimerManager); + +// 创建定时器 +timerManager.schedule(1.0, false, null, (timer) => { + console.log('1秒后执行'); +}); +``` + +### PerformanceMonitor + +性能监控器,监控游戏性能并提供优化建议: + +```typescript +const monitor = Core.services.resolve(PerformanceMonitor); + +// 启用性能监控 +monitor.enable(); + +// 获取性能数据 +const fps = monitor.getFPS(); +``` + +### SceneManager + +场景管理器,管理场景的生命周期: + +```typescript +const sceneManager = Core.services.resolve(SceneManager); + +// 获取当前场景 +const currentScene = sceneManager.currentScene; +``` + +### PoolManager + +对象池管理器,管理所有对象池: + +```typescript +const poolManager = Core.services.resolve(PoolManager); + +// 创建对象池 +const bulletPool = poolManager.createPool('bullets', () => new Bullet(), 100); +``` + +### PluginManager + +插件管理器,管理插件的安装和卸载: + +```typescript +const pluginManager = Core.services.resolve(PluginManager); + +// 获取所有已安装的插件 +const plugins = pluginManager.getAllPlugins(); +``` + +## 依赖注入 + +ECS Framework 提供了装饰器来简化依赖注入。 + +### @Injectable 装饰器 + +标记类为可注入的服务: + +```typescript +import { Injectable, IService } from '@esengine/ecs-framework'; + +@Injectable() +class GameService implements IService { + constructor() { + console.log('GameService created'); + } + + dispose(): void { + console.log('GameService disposed'); + } +} +``` + +### @Inject 装饰器 + +在构造函数中注入依赖: + +```typescript +import { Injectable, Inject, IService } from '@esengine/ecs-framework'; + +@Injectable() +class PlayerService implements IService { + constructor( + @Inject(DataService) private data: DataService, + @Inject(GameService) private game: GameService + ) { + // data 和 game 会自动从容器中解析 + } + + dispose(): void { + // 清理资源 + } +} +``` + +### 注册可注入服务 + +使用 `registerInjectable` 自动处理依赖注入: + +```typescript +import { registerInjectable } from '@esengine/ecs-framework'; + +// 注册服务(会自动解析@Inject依赖) +registerInjectable(Core.services, PlayerService); + +// 解析时会自动注入依赖 +const player = Core.services.resolve(PlayerService); +``` + +### @Updatable 装饰器 + +标记服务为可更新的,使其在每帧自动被调用: + +```typescript +import { Injectable, Updatable, IService, IUpdatable } from '@esengine/ecs-framework'; + +@Injectable() +@Updatable() // 默认优先级为0 +class PhysicsService implements IService, IUpdatable { + update(deltaTime?: number): void { + // 每帧更新物理模拟 + } + + dispose(): void { + // 清理资源 + } +} + +// 指定更新优先级(数值越小越先执行) +@Injectable() +@Updatable(10) +class RenderService implements IService, IUpdatable { + update(deltaTime?: number): void { + // 每帧渲染 + } + + dispose(): void { + // 清理资源 + } +} +``` + +使用 `@Updatable` 装饰器的服务会被 Core 自动调用,无需手动管理: + +```typescript +// Core.update() 会自动调用所有@Updatable服务的update方法 +function gameLoop(deltaTime: number) { + Core.update(deltaTime); // 自动更新所有可更新服务 +} +``` + +## 自定义服务 + +### 创建自定义服务 + +实现 `IService` 接口并注册到容器: + +```typescript +import { IService } from '@esengine/ecs-framework'; + +class AudioService implements IService { + private sounds: Map = new Map(); + + play(soundId: string) { + const sound = this.sounds.get(soundId); + if (sound) { + sound.play(); + } + } + + load(soundId: string, url: string) { + const audio = new Audio(url); + this.sounds.set(soundId, audio); + } + + dispose(): void { + // 停止所有音效并清理 + for (const sound of this.sounds.values()) { + sound.pause(); + sound.src = ''; + } + this.sounds.clear(); + } +} + +// 注册自定义服务 +Core.services.registerSingleton(AudioService); + +// 使用服务 +const audio = Core.services.resolve(AudioService); +audio.load('jump', '/sounds/jump.mp3'); +audio.play('jump'); +``` + +### 服务间依赖 + +服务可以依赖其他服务: + +```typescript +@Injectable() +class ConfigService implements IService { + private config: any = {}; + + get(key: string) { + return this.config[key]; + } + + dispose(): void { + this.config = {}; + } +} + +@Injectable() +class NetworkService implements IService { + constructor( + @Inject(ConfigService) private config: ConfigService + ) { + // 使用配置服务 + const apiUrl = this.config.get('apiUrl'); + } + + dispose(): void { + // 清理网络连接 + } +} + +// 注册服务(按依赖顺序) +registerInjectable(Core.services, ConfigService); +registerInjectable(Core.services, NetworkService); +``` + +## 高级用法 + +### 服务替换(测试) + +在测试中替换真实服务为模拟服务: + +```typescript +// 测试代码 +class MockDataService implements IService { + getData(key: string) { + return 'mock data'; + } + + dispose(): void {} +} + +// 注册模拟服务(用于测试) +Core.services.registerInstance(DataService, new MockDataService()); +``` + +### 循环依赖检测 + +服务容器会自动检测循环依赖: + +```typescript +// A 依赖 B,B 依赖 A +@Injectable() +class ServiceA implements IService { + constructor(@Inject(ServiceB) b: ServiceB) {} + dispose(): void {} +} + +@Injectable() +class ServiceB implements IService { + constructor(@Inject(ServiceA) a: ServiceA) {} + dispose(): void {} +} + +// 解析时会抛出错误: Circular dependency detected: ServiceA -> ServiceB -> ServiceA +``` + +### 获取所有服务 + +```typescript +// 获取所有已注册的服务类型 +const types = Core.services.getRegisteredServices(); + +// 获取所有已实例化的服务实例 +const instances = Core.services.getAll(); +``` + +### 服务清理 + +```typescript +// 注销单个服务 +Core.services.unregister(MyService); + +// 清空所有服务(会调用每个服务的dispose方法) +Core.services.clear(); +``` + +## 最佳实践 + +### 服务命名 + +服务类名应该以 `Service` 或 `Manager` 结尾,清晰表达其职责: + +```typescript +class PlayerService implements IService {} +class AudioManager implements IService {} +class NetworkService implements IService {} +``` + +### 资源清理 + +始终在 `dispose()` 方法中清理资源: + +```typescript +class ResourceService implements IService { + private resources: Map = new Map(); + + dispose(): void { + // 释放所有资源 + for (const resource of this.resources.values()) { + resource.release(); + } + this.resources.clear(); + } +} +``` + +### 避免过度使用 + +不要把所有类都注册为服务,服务应该是: + +- 全局单例或需要共享状态 +- 需要在多处使用 +- 生命周期需要管理 +- 需要依赖注入 + +对于简单的工具类或数据类,直接创建实例即可。 + +### 依赖方向 + +保持清晰的依赖方向,避免循环依赖: + +``` +高层服务 -> 中层服务 -> 底层服务 +GameLogic -> DataService -> ConfigService +``` + +## 常见问题 + +### 服务未注册错误 + +**问题**: `Error: Service MyService is not registered` + +**解决**: +```typescript +// 确保服务已注册 +Core.services.registerSingleton(MyService); + +// 或者使用tryResolve +const service = Core.services.tryResolve(MyService); +if (!service) { + console.log('Service not found'); +} +``` + +### 循环依赖错误 + +**问题**: `Circular dependency detected` + +**解决**: 重新设计服务依赖关系,引入中间服务或使用事件系统解耦。 + +### 何时使用单例 vs 瞬时 + +- **单例**: 管理器类、配置、缓存、状态管理 +- **瞬时**: 命令对象、请求处理器、临时任务 + +## 相关链接 + +- [插件系统](./plugin-system.md) - 使用服务容器注册插件服务 +- [快速开始](./getting-started.md) - Core 初始化和基础使用 +- [系统架构](./system.md) - 在系统中使用服务 diff --git a/docs/guide/system.md b/docs/guide/system.md index b7845cc7..180f01ae 100644 --- a/docs/guide/system.md +++ b/docs/guide/system.md @@ -354,14 +354,18 @@ class PerformanceSystem extends EntitySystem { ### 添加系统到场景 +框架提供了两种方式添加系统:传入实例或传入类型(自动依赖注入)。 + ```typescript // 在场景子类中添加系统 class GameScene extends Scene { protected initialize(): void { - // 添加系统 + // 方式1:传入实例 this.addSystem(new MovementSystem()); this.addSystem(new RenderSystem()); - this.addSystem(new PhysicsSystem()); + + // 方式2:传入类型(自动依赖注入) + this.addEntityProcessor(PhysicsSystem); // 设置系统更新顺序 const movementSystem = this.getSystem(MovementSystem); @@ -372,6 +376,48 @@ class GameScene extends Scene { } ``` +### 系统依赖注入 + +系统实现了 `IService` 接口,支持通过依赖注入获取其他服务或系统: + +```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 { + // 使用注入的服务 + this.collision.detectCollisions(entities); + } + + // 实现 IService 接口的 dispose 方法 + public dispose(): void { + // 清理资源 + } +} + +// 使用时传入类型即可,框架会自动注入依赖 +class GameScene extends Scene { + protected initialize(): void { + // 自动依赖注入 + this.addEntityProcessor(PhysicsSystem); + } +} +``` + +注意事项: +- 使用 `@Injectable()` 装饰器标记需要依赖注入的系统 +- 在构造函数参数中使用 `@Inject()` 装饰器声明依赖 +- 系统必须实现 `dispose()` 方法(IService 接口要求) +- 使用 `addEntityProcessor(类型)` 而不是 `addSystem(new 类型())` 来启用依赖注入 + ### 系统更新顺序 ```typescript diff --git a/package-lock.json b/package-lock.json index 51fcc889..53d72ecb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15557,7 +15557,7 @@ }, "packages/core": { "name": "@esengine/ecs-framework", - "version": "2.1.52", + "version": "2.2.0", "license": "MIT", "dependencies": { "msgpack-lite": "^0.1.26" diff --git a/packages/core/package.json b/packages/core/package.json index f171f4c9..a0c6642a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@esengine/ecs-framework", - "version": "2.1.52", + "version": "2.2.0", "description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架", "main": "bin/index.js", "types": "bin/index.d.ts",