服务容器
服务容器(ServiceContainer)是 ECS Framework 的依赖注入容器,负责管理框架中所有服务的注册、解析和生命周期。通过服务容器,你可以实现松耦合的架构设计,提高代码的可测试性和可维护性。
概述
什么是服务容器
服务容器是一个轻量级的依赖注入(DI)容器,它提供了:
- 服务注册: 将服务类型注册到容器中
- 服务解析: 从容器中获取服务实例
- 生命周期管理: 自动管理服务实例的创建和销毁
- 依赖注入: 自动解析服务之间的依赖关系
核心概念
服务(Service)
服务是实现了 IService 接口的类,必须提供 dispose() 方法用于资源清理:
import { IService } from '@esengine/ecs-framework';
class MyService implements IService {
constructor() {
// 初始化逻辑
}
dispose(): void {
// 清理资源
}
}生命周期
服务容器支持两种生命周期:
- Singleton(单例): 整个应用程序生命周期内只有一个实例,所有解析请求返回同一个实例
- Transient(瞬时): 每次解析都创建新的实例
基础使用
访问服务容器
Core 类内置了服务容器,可以通过 Core.services 访问:
import { Core } from '@esengine/ecs-framework';
// 初始化Core
Core.create({ debug: true });
// 访问服务容器
const container = Core.services;注册服务
注册单例服务
单例服务在首次解析时创建,之后所有解析请求都返回同一个实例:
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);注册瞬时服务
瞬时服务每次解析都创建新实例,适用于无状态或短生命周期的服务:
class CommandService implements IService {
execute(command: string) {
console.log(`Executing: ${command}`);
}
dispose(): void {
// 清理资源
}
}
// 注册瞬时服务
Core.services.registerTransient(CommandService);注册服务实例
直接注册已创建的实例,自动视为单例:
const config = new ConfigService();
config.load('./config.json');
// 注册实例
Core.services.registerInstance(ConfigService, config);使用工厂函数注册
工厂函数允许你在创建服务时执行自定义逻辑:
Core.services.registerSingleton(LoggerService, (container) => {
const logger = new LoggerService();
logger.setLevel('debug');
return logger;
});解析服务
resolve 方法
解析服务实例,如果服务未注册会抛出异常:
// 解析服务
const dataService = Core.services.resolve(DataService);
dataService.setData('player', { name: 'Alice', score: 100 });
// 单例服务,多次解析返回同一个实例
const same = Core.services.resolve(DataService);
console.log(same === dataService); // truetryResolve 方法
尝试解析服务,如果未注册返回 null 而不抛出异常:
const optional = Core.services.tryResolve(OptionalService);
if (optional) {
optional.doSomething();
}isRegistered 方法
检查服务是否已注册:
if (Core.services.isRegistered(DataService)) {
const service = Core.services.resolve(DataService);
}内置服务
Core 在初始化时自动注册了以下内置服务:
TimerManager
定时器管理器,负责管理所有游戏定时器:
const timerManager = Core.services.resolve(TimerManager);
// 创建定时器
timerManager.schedule(1.0, false, null, (timer) => {
console.log('1秒后执行');
});PerformanceMonitor
性能监控器,监控游戏性能并提供优化建议:
const monitor = Core.services.resolve(PerformanceMonitor);
// 启用性能监控
monitor.enable();
// 获取性能数据
const fps = monitor.getFPS();SceneManager
场景管理器,管理单场景应用的场景生命周期:
const sceneManager = Core.services.resolve(SceneManager);
// 设置当前场景
sceneManager.setScene(new GameScene());
// 获取当前场景
const currentScene = sceneManager.currentScene;
// 延迟切换场景
sceneManager.loadScene(new MenuScene());
// 更新场景
sceneManager.update();WorldManager
世界管理器,管理多个独立的 World 实例(高级用例):
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 服务器、游戏房间系统等需要完全隔离的多世界应用
PoolManager
对象池管理器,管理所有对象池:
const poolManager = Core.services.resolve(PoolManager);
// 创建对象池
const bulletPool = poolManager.createPool('bullets', () => new Bullet(), 100);PluginManager
插件管理器,管理插件的安装和卸载:
const pluginManager = Core.services.resolve(PluginManager);
// 获取所有已安装的插件
const plugins = pluginManager.getAllPlugins();依赖注入
ECS Framework 提供了装饰器来简化依赖注入。
@Injectable 装饰器
标记类为可注入的服务:
import { Injectable, IService } from '@esengine/ecs-framework';
@Injectable()
class GameService implements IService {
constructor() {
console.log('GameService created');
}
dispose(): void {
console.log('GameService disposed');
}
}@Inject 装饰器
在构造函数中注入依赖:
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 自动处理依赖注入:
import { registerInjectable } from '@esengine/ecs-framework';
// 注册服务(会自动解析@Inject依赖)
registerInjectable(Core.services, PlayerService);
// 解析时会自动注入依赖
const player = Core.services.resolve(PlayerService);@Updatable 装饰器
标记服务为可更新的,使其在每帧自动被调用:
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 自动调用,无需手动管理:
// Core.update() 会自动调用所有@Updatable服务的update方法
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新所有可更新服务
}自定义服务
创建自定义服务
实现 IService 接口并注册到容器:
import { IService } from '@esengine/ecs-framework';
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');服务间依赖
服务可以依赖其他服务:
@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);高级用法
服务替换(测试)
在测试中替换真实服务为模拟服务:
// 测试代码
class MockDataService implements IService {
getData(key: string) {
return 'mock data';
}
dispose(): void {}
}
// 注册模拟服务(用于测试)
Core.services.registerInstance(DataService, new MockDataService());循环依赖检测
服务容器会自动检测循环依赖:
// 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获取所有服务
// 获取所有已注册的服务类型
const types = Core.services.getRegisteredServices();
// 获取所有已实例化的服务实例
const instances = Core.services.getAll();服务清理
// 注销单个服务
Core.services.unregister(MyService);
// 清空所有服务(会调用每个服务的dispose方法)
Core.services.clear();最佳实践
服务命名
服务类名应该以 Service 或 Manager 结尾,清晰表达其职责:
class PlayerService implements IService {}
class AudioManager implements IService {}
class NetworkService implements IService {}资源清理
始终在 dispose() 方法中清理资源:
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
解决:
// 确保服务已注册
Core.services.registerSingleton(MyService);
// 或者使用tryResolve
const service = Core.services.tryResolve(MyService);
if (!service) {
console.log('Service not found');
}循环依赖错误
问题: Circular dependency detected
解决: 重新设计服务依赖关系,引入中间服务或使用事件系统解耦。
何时使用单例 vs 瞬时
- 单例: 管理器类、配置、缓存、状态管理
- 瞬时: 命令对象、请求处理器、临时任务