2025-10-11 10:16:52 +08:00
|
|
|
|
# 服务容器
|
|
|
|
|
|
|
|
|
|
|
|
服务容器(ServiceContainer)是 ECS Framework 的依赖注入容器,负责管理框架中所有服务的注册、解析和生命周期。通过服务容器,你可以实现松耦合的架构设计,提高代码的可测试性和可维护性。
|
|
|
|
|
|
|
|
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
|
|
### 什么是服务容器
|
|
|
|
|
|
|
|
|
|
|
|
服务容器是一个轻量级的依赖注入(DI)容器,它提供了:
|
|
|
|
|
|
|
|
|
|
|
|
- **服务注册**: 将服务类型注册到容器中
|
|
|
|
|
|
- **服务解析**: 从容器中获取服务实例
|
|
|
|
|
|
- **生命周期管理**: 自动管理服务实例的创建和销毁
|
|
|
|
|
|
- **依赖注入**: 自动解析服务之间的依赖关系
|
|
|
|
|
|
|
|
|
|
|
|
### 核心概念
|
|
|
|
|
|
|
|
|
|
|
|
#### 服务(Service)
|
|
|
|
|
|
|
|
|
|
|
|
服务是实现了 `IService` 接口的类,必须提供 `dispose()` 方法用于资源清理:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-08 21:26:35 +08:00
|
|
|
|
import { IService } from '@esengine/ecs-framework';
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
class MyService implements IService {
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
// 初始化逻辑
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dispose(): void {
|
|
|
|
|
|
// 清理资源
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
#### 服务标识符(ServiceIdentifier)
|
|
|
|
|
|
|
|
|
|
|
|
服务标识符用于在容器中唯一标识一个服务,支持两种类型:
|
|
|
|
|
|
|
|
|
|
|
|
- **类构造函数**: 直接使用服务类作为标识符,适用于具体实现类
|
|
|
|
|
|
- **Symbol**: 使用 Symbol 作为标识符,适用于接口抽象(推荐用于插件和跨包场景)
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 方式1: 使用类作为标识符
|
|
|
|
|
|
Core.services.registerSingleton(DataService);
|
|
|
|
|
|
const data = Core.services.resolve(DataService);
|
|
|
|
|
|
|
|
|
|
|
|
// 方式2: 使用 Symbol 作为标识符(推荐用于接口)
|
|
|
|
|
|
const IFileSystem = Symbol.for('IFileSystem');
|
|
|
|
|
|
Core.services.registerInstance(IFileSystem, new TauriFileSystem());
|
|
|
|
|
|
const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> **提示**: 使用 `Symbol.for()` 而非 `Symbol()` 可确保跨包/跨模块共享同一个标识符。详见[高级用法 - 接口与 Symbol 标识符模式](#接口与-symbol-标识符模式)。
|
|
|
|
|
|
|
2025-10-11 10:16:52 +08:00
|
|
|
|
#### 生命周期
|
|
|
|
|
|
|
|
|
|
|
|
服务容器支持两种生命周期:
|
|
|
|
|
|
|
|
|
|
|
|
- **Singleton(单例)**: 整个应用程序生命周期内只有一个实例,所有解析请求返回同一个实例
|
|
|
|
|
|
- **Transient(瞬时)**: 每次解析都创建新的实例
|
|
|
|
|
|
|
|
|
|
|
|
## 基础使用
|
|
|
|
|
|
|
|
|
|
|
|
### 访问服务容器
|
|
|
|
|
|
|
2025-11-14 09:55:31 +08:00
|
|
|
|
ECS Framework 提供了三级服务容器:
|
|
|
|
|
|
|
|
|
|
|
|
> **版本说明**:World 服务容器功能在 v2.2.13+ 版本中可用
|
|
|
|
|
|
|
|
|
|
|
|
#### Core 级别服务容器
|
|
|
|
|
|
|
|
|
|
|
|
应用程序全局服务容器,可以通过 `Core.services` 访问:
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-08 21:26:35 +08:00
|
|
|
|
import { Core } from '@esengine/ecs-framework';
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化Core
|
|
|
|
|
|
Core.create({ debug: true });
|
|
|
|
|
|
|
2025-11-14 09:55:31 +08:00
|
|
|
|
// 访问全局服务容器
|
2025-10-11 10:16:52 +08:00
|
|
|
|
const container = Core.services;
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2025-11-14 09:55:31 +08:00
|
|
|
|
#### World 级别服务容器
|
|
|
|
|
|
|
|
|
|
|
|
每个 World 拥有独立的服务容器,用于管理 World 范围内的服务:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-08 21:26:35 +08:00
|
|
|
|
import { World } from '@esengine/ecs-framework';
|
2025-11-14 09:55:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建 World
|
|
|
|
|
|
const world = new World({ name: 'GameWorld' });
|
|
|
|
|
|
|
|
|
|
|
|
// 访问 World 级别的服务容器
|
|
|
|
|
|
const worldContainer = world.services;
|
|
|
|
|
|
|
|
|
|
|
|
// 注册 World 级别的服务
|
|
|
|
|
|
world.services.registerSingleton(RoomManager);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### Scene 级别服务容器
|
|
|
|
|
|
|
|
|
|
|
|
每个 Scene 拥有独立的服务容器,用于管理 Scene 范围内的服务:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 访问 Scene 级别的服务容器
|
|
|
|
|
|
const sceneContainer = scene.services;
|
|
|
|
|
|
|
|
|
|
|
|
// 注册 Scene 级别的服务
|
|
|
|
|
|
scene.services.registerSingleton(PhysicsSystem);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 服务容器层级
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
Core.services (应用程序全局)
|
|
|
|
|
|
└─ World.services (World 级别)
|
|
|
|
|
|
└─ Scene.services (Scene 级别)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
不同级别的服务容器是独立的,服务不会自动向上或向下查找。选择合适的容器级别:
|
|
|
|
|
|
|
|
|
|
|
|
- **Core.services**: 应用程序级别的全局服务(配置、插件管理器等)
|
|
|
|
|
|
- **World.services**: World 级别的服务(房间管理器、多人游戏状态等)
|
|
|
|
|
|
- **Scene.services**: Scene 级别的服务(ECS 系统、场景特定逻辑等)
|
|
|
|
|
|
|
2025-10-11 10:16:52 +08:00
|
|
|
|
### 注册服务
|
|
|
|
|
|
|
|
|
|
|
|
#### 注册单例服务
|
|
|
|
|
|
|
|
|
|
|
|
单例服务在首次解析时创建,之后所有解析请求都返回同一个实例:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
class DataService implements IService {
|
|
|
|
|
|
private data: Map<string, any> = 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
|
|
|
|
|
|
|
2025-10-11 10:48:24 +08:00
|
|
|
|
场景管理器,管理单场景应用的场景生命周期:
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
const sceneManager = Core.services.resolve(SceneManager);
|
|
|
|
|
|
|
2025-10-11 10:48:24 +08:00
|
|
|
|
// 设置当前场景
|
|
|
|
|
|
sceneManager.setScene(new GameScene());
|
|
|
|
|
|
|
2025-10-11 10:16:52 +08:00
|
|
|
|
// 获取当前场景
|
|
|
|
|
|
const currentScene = sceneManager.currentScene;
|
2025-10-11 10:48:24 +08:00
|
|
|
|
|
|
|
|
|
|
// 延迟切换场景
|
|
|
|
|
|
sceneManager.loadScene(new MenuScene());
|
|
|
|
|
|
|
|
|
|
|
|
// 更新场景
|
|
|
|
|
|
sceneManager.update();
|
2025-10-11 10:16:52 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2025-10-11 10:48:24 +08:00
|
|
|
|
### WorldManager
|
|
|
|
|
|
|
|
|
|
|
|
世界管理器,管理多个独立的 World 实例(高级用例):
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
const worldManager = Core.services.resolve(WorldManager);
|
|
|
|
|
|
|
|
|
|
|
|
// 创建独立的游戏世界
|
|
|
|
|
|
const gameWorld = worldManager.createWorld('game_room_001', {
|
|
|
|
|
|
name: 'GameRoom',
|
|
|
|
|
|
maxScenes: 5
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 在World中创建场景
|
|
|
|
|
|
const scene = gameWorld.createScene('battle', new BattleScene());
|
|
|
|
|
|
gameWorld.setSceneActive('battle', true);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新所有World
|
|
|
|
|
|
worldManager.updateAll();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**适用场景**:
|
|
|
|
|
|
- SceneManager: 适用于 95% 的游戏(单人游戏、简单多人游戏)
|
|
|
|
|
|
- WorldManager: 适用于 MMO 服务器、游戏房间系统等需要完全隔离的多世界应用
|
|
|
|
|
|
|
2025-10-11 10:16:52 +08:00
|
|
|
|
### 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
|
2025-12-08 21:26:35 +08:00
|
|
|
|
import { Injectable, IService } from '@esengine/ecs-framework';
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
class GameService implements IService {
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
console.log('GameService created');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dispose(): void {
|
|
|
|
|
|
console.log('GameService disposed');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
### @InjectProperty 装饰器
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成:
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-08 21:26:35 +08:00
|
|
|
|
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
class PlayerService implements IService {
|
2025-11-27 20:42:46 +08:00
|
|
|
|
@InjectProperty(DataService)
|
|
|
|
|
|
private data!: DataService;
|
|
|
|
|
|
|
|
|
|
|
|
@InjectProperty(GameService)
|
|
|
|
|
|
private game!: GameService;
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
dispose(): void {
|
|
|
|
|
|
// 清理资源
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
在 EntitySystem 中使用属性注入:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
class CombatSystem extends EntitySystem {
|
|
|
|
|
|
@InjectProperty(TimeService)
|
|
|
|
|
|
private timeService!: TimeService;
|
|
|
|
|
|
|
|
|
|
|
|
@InjectProperty(AudioService)
|
|
|
|
|
|
private audio!: AudioService;
|
|
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
super(Matcher.all(Health, Attack));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onInitialize(): void {
|
|
|
|
|
|
// 此时属性已注入完成,可以安全使用
|
|
|
|
|
|
console.log('Delta time:', this.timeService.getDeltaTime());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
processEntity(entity: Entity): void {
|
|
|
|
|
|
// 使用注入的服务
|
|
|
|
|
|
this.audio.playSound('attack');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> **注意**: 属性声明时使用 `!` 断言(如 `private data!: DataService`),表示该属性会在使用前被注入。
|
|
|
|
|
|
|
2025-10-11 10:16:52 +08:00
|
|
|
|
### 注册可注入服务
|
|
|
|
|
|
|
|
|
|
|
|
使用 `registerInjectable` 自动处理依赖注入:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-08 21:26:35 +08:00
|
|
|
|
import { registerInjectable } from '@esengine/ecs-framework';
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// 注册服务(会自动解析 @InjectProperty 依赖)
|
2025-10-11 10:16:52 +08:00
|
|
|
|
registerInjectable(Core.services, PlayerService);
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// 解析时会自动注入属性依赖
|
2025-10-11 10:16:52 +08:00
|
|
|
|
const player = Core.services.resolve(PlayerService);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### @Updatable 装饰器
|
|
|
|
|
|
|
|
|
|
|
|
标记服务为可更新的,使其在每帧自动被调用:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-12-08 21:26:35 +08:00
|
|
|
|
import { Injectable, Updatable, IService, IUpdatable } from '@esengine/ecs-framework';
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
@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
|
2025-12-08 21:26:35 +08:00
|
|
|
|
import { IService } from '@esengine/ecs-framework';
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
class AudioService implements IService {
|
|
|
|
|
|
private sounds: Map<string, HTMLAudioElement> = 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);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 高级用法
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
### 接口与 Symbol 标识符模式
|
|
|
|
|
|
|
|
|
|
|
|
在大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
|
|
|
|
|
|
|
|
|
|
|
|
#### 为什么使用 Symbol.for
|
|
|
|
|
|
|
|
|
|
|
|
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol,确保不同包中使用相同的标识符
|
|
|
|
|
|
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
|
|
|
|
|
|
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配)
|
|
|
|
|
|
|
|
|
|
|
|
#### 定义接口和标识符
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
以音频服务为例,游戏需要在不同平台(Web、微信小游戏、原生App)使用不同的音频实现:
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
```typescript
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// IAudioService.ts - 定义接口和标识符
|
|
|
|
|
|
export interface IAudioService {
|
|
|
|
|
|
dispose(): void;
|
|
|
|
|
|
playSound(id: string): void;
|
|
|
|
|
|
playMusic(id: string, loop?: boolean): void;
|
|
|
|
|
|
stopMusic(): void;
|
|
|
|
|
|
setVolume(volume: number): void;
|
|
|
|
|
|
preload(id: string, url: string): Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 Symbol.for 确保跨包共享同一个 Symbol
|
|
|
|
|
|
export const IAudioService = Symbol.for('IAudioService');
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 实现接口
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// WebAudioService.ts - Web 平台实现
|
|
|
|
|
|
import { IAudioService } from './IAudioService';
|
|
|
|
|
|
|
|
|
|
|
|
export class WebAudioService implements IAudioService {
|
|
|
|
|
|
private audioContext: AudioContext;
|
|
|
|
|
|
private sounds: Map<string, AudioBuffer> = new Map();
|
|
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.audioContext = new AudioContext();
|
2025-10-11 10:16:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
playSound(id: string): void {
|
|
|
|
|
|
const buffer = this.sounds.get(id);
|
|
|
|
|
|
if (buffer) {
|
|
|
|
|
|
const source = this.audioContext.createBufferSource();
|
|
|
|
|
|
source.buffer = buffer;
|
|
|
|
|
|
source.connect(this.audioContext.destination);
|
|
|
|
|
|
source.start();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async preload(id: string, url: string): Promise<void> {
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
|
|
|
|
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
|
|
|
|
this.sounds.set(id, audioBuffer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ... 其他方法实现
|
|
|
|
|
|
|
|
|
|
|
|
dispose(): void {
|
|
|
|
|
|
this.audioContext.close();
|
|
|
|
|
|
this.sounds.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// WechatAudioService.ts - 微信小游戏平台实现
|
|
|
|
|
|
export class WechatAudioService implements IAudioService {
|
|
|
|
|
|
private innerAudioContexts: Map<string, WechatMinigame.InnerAudioContext> = new Map();
|
|
|
|
|
|
|
|
|
|
|
|
playSound(id: string): void {
|
|
|
|
|
|
const ctx = this.innerAudioContexts.get(id);
|
|
|
|
|
|
if (ctx) {
|
|
|
|
|
|
ctx.play();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async preload(id: string, url: string): Promise<void> {
|
|
|
|
|
|
const ctx = wx.createInnerAudioContext();
|
|
|
|
|
|
ctx.src = url;
|
|
|
|
|
|
this.innerAudioContexts.set(id, ctx);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ... 其他方法实现
|
|
|
|
|
|
|
|
|
|
|
|
dispose(): void {
|
|
|
|
|
|
for (const ctx of this.innerAudioContexts.values()) {
|
|
|
|
|
|
ctx.destroy();
|
|
|
|
|
|
}
|
|
|
|
|
|
this.innerAudioContexts.clear();
|
|
|
|
|
|
}
|
2025-10-11 10:16:52 +08:00
|
|
|
|
}
|
2025-11-27 20:42:46 +08:00
|
|
|
|
```
|
2025-10-11 10:16:52 +08:00
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
#### 注册和使用
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
import { IAudioService } from './IAudioService';
|
|
|
|
|
|
import { WebAudioService } from './WebAudioService';
|
|
|
|
|
|
import { WechatAudioService } from './WechatAudioService';
|
|
|
|
|
|
|
|
|
|
|
|
// 根据平台注册不同实现
|
|
|
|
|
|
if (typeof wx !== 'undefined') {
|
|
|
|
|
|
Core.services.registerInstance(IAudioService, new WechatAudioService());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Core.services.registerInstance(IAudioService, new WebAudioService());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 业务代码中使用 - 不关心具体实现
|
|
|
|
|
|
const audio = Core.services.resolve<IAudioService>(IAudioService);
|
|
|
|
|
|
await audio.preload('explosion', '/sounds/explosion.mp3');
|
|
|
|
|
|
audio.playSound('explosion');
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 跨模块使用
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 在游戏系统中使用
|
|
|
|
|
|
import { IAudioService } from '@mygame/core';
|
|
|
|
|
|
|
|
|
|
|
|
class CombatSystem extends EntitySystem {
|
|
|
|
|
|
private audio: IAudioService;
|
|
|
|
|
|
|
|
|
|
|
|
initialize(): void {
|
|
|
|
|
|
// 获取音频服务,不需要知道具体实现
|
|
|
|
|
|
this.audio = this.scene.services.resolve<IAudioService>(IAudioService);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onEntityDeath(entity: Entity): void {
|
|
|
|
|
|
this.audio.playSound('death');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### Symbol vs Symbol.for
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// Symbol() - 每次创建唯一的 Symbol
|
|
|
|
|
|
const sym1 = Symbol('test');
|
|
|
|
|
|
const sym2 = Symbol('test');
|
|
|
|
|
|
console.log(sym1 === sym2); // false - 不同的 Symbol
|
|
|
|
|
|
|
|
|
|
|
|
// Symbol.for() - 在全局注册表中共享
|
|
|
|
|
|
const sym3 = Symbol.for('test');
|
|
|
|
|
|
const sym4 = Symbol.for('test');
|
|
|
|
|
|
console.log(sym3 === sym4); // true - 同一个 Symbol
|
|
|
|
|
|
|
|
|
|
|
|
// 跨包场景
|
|
|
|
|
|
// package-a/index.ts
|
|
|
|
|
|
export const IMyService = Symbol.for('IMyService');
|
|
|
|
|
|
|
|
|
|
|
|
// package-b/index.ts (不同的包)
|
|
|
|
|
|
const IMyService = Symbol.for('IMyService');
|
|
|
|
|
|
// 与 package-a 中的是同一个 Symbol!
|
2025-10-11 10:16:52 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 循环依赖检测
|
|
|
|
|
|
|
|
|
|
|
|
服务容器会自动检测循环依赖:
|
|
|
|
|
|
|
|
|
|
|
|
```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<string, Resource> = 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) - 在系统中使用服务
|