diff --git a/.changeset/cli-server-adapter.md b/.changeset/cli-server-adapter.md new file mode 100644 index 00000000..d52f62fa --- /dev/null +++ b/.changeset/cli-server-adapter.md @@ -0,0 +1,16 @@ +--- +"@esengine/cli": minor +"@esengine/network-server": patch +--- + +feat(cli): 增强 Node.js 服务端适配器 + +**@esengine/cli:** +- 添加 @esengine/network-server 依赖支持 +- 生成完整的 ECS 游戏服务器项目结构 +- 组件使用 @ECSComponent 装饰器注册 +- tsconfig 启用 experimentalDecorators + +**@esengine/network-server:** +- 支持 ESM/CJS 双格式导出 +- 添加 ws@8.18.0 解决 Node.js 24 兼容性问题 diff --git a/docs/en/modules/network/index.md b/docs/en/modules/network/index.md index ff76d878..8ef0c571 100644 --- a/docs/en/modules/network/index.md +++ b/docs/en/modules/network/index.md @@ -22,44 +22,112 @@ npm install @esengine/network npm install @esengine/network-server ``` +## Quick Setup with CLI + +We recommend using ESEngine CLI to quickly create a complete game server project: + +```bash +# Create project directory +mkdir my-game-server && cd my-game-server +npm init -y + +# Initialize Node.js server with CLI +npx @esengine/cli init -p nodejs +``` + +The CLI will generate the following project structure: + +``` +my-game-server/ +├── src/ +│ ├── index.ts # Entry point +│ ├── server/ +│ │ └── GameServer.ts # Network server configuration +│ └── game/ +│ ├── Game.ts # ECS game class +│ ├── scenes/ +│ │ └── MainScene.ts # Main scene +│ ├── components/ # ECS components +│ │ ├── PositionComponent.ts +│ │ └── VelocityComponent.ts +│ └── systems/ # ECS systems +│ └── MovementSystem.ts +├── tsconfig.json +├── package.json +└── README.md +``` + +Start the server: + +```bash +# Development mode (hot reload) +npm run dev + +# Production mode +npm run start +``` + ## Quick Start ### Client ```typescript -import { World } from '@esengine/ecs-framework'; +import { Core, Scene } from '@esengine/ecs-framework'; import { NetworkPlugin, NetworkIdentity, NetworkTransform } from '@esengine/network'; -// Create World and install network plugin -const world = new World(); -const networkPlugin = new NetworkPlugin({ - serverUrl: 'ws://localhost:3000' -}); -networkPlugin.install(world.services); +// Define game scene +class GameScene extends Scene { + initialize(): void { + this.name = 'Game'; + // Network systems are automatically added by NetworkPlugin + } +} + +// Initialize Core +Core.create({ debug: false }); +const scene = new GameScene(); +Core.setScene(scene); + +// Install network plugin +const networkPlugin = new NetworkPlugin(); +await Core.installPlugin(networkPlugin); // Register prefab factory -networkPlugin.registerPrefab('player', (netId, ownerId) => { - const entity = world.createEntity(`player_${netId}`); - entity.addComponent(new NetworkIdentity(netId, ownerId)); +networkPlugin.registerPrefab('player', (scene, spawn) => { + const entity = scene.createEntity(`player_${spawn.netId}`); + + const identity = entity.addComponent(new NetworkIdentity()); + identity.netId = spawn.netId; + identity.ownerId = spawn.ownerId; + identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId; + entity.addComponent(new NetworkTransform()); - // Add other components... return entity; }); // Connect to server -await networkPlugin.connect('PlayerName'); -console.log('Connected! Client ID:', networkPlugin.localPlayerId); +const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName'); +if (success) { + console.log('Connected!'); +} + +// Game loop +function gameLoop(dt: number) { + Core.update(dt); +} // Disconnect -networkPlugin.disconnect(); +await networkPlugin.disconnect(); ``` ### Server +After creating a server project with CLI, the generated code already configures GameServer: + ```typescript import { GameServer } from '@esengine/network-server'; @@ -72,6 +140,7 @@ const server = new GameServer({ }); await server.start(); +console.log('Server started on ws://localhost:3000'); ``` ## Core Concepts @@ -229,15 +298,19 @@ interface INetworkCallbacks { ### Prefab Factory ```typescript -type PrefabFactory = (netId: number, ownerId: number) => Entity; +type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity; ``` Register prefab factories for network entity creation: ```typescript -networkPlugin.registerPrefab('enemy', (netId, ownerId) => { - const entity = world.createEntity(`enemy_${netId}`); - entity.addComponent(new NetworkIdentity(netId, ownerId)); +networkPlugin.registerPrefab('enemy', (scene, spawn) => { + const entity = scene.createEntity(`enemy_${spawn.netId}`); + + const identity = entity.addComponent(new NetworkIdentity()); + identity.netId = spawn.netId; + identity.ownerId = spawn.ownerId; + entity.addComponent(new NetworkTransform()); entity.addComponent(new EnemyComponent()); return entity; @@ -264,9 +337,12 @@ class NetworkInputSystem extends EntitySystem { Usage example: ```typescript -const inputSystem = world.getSystem(NetworkInputSystem); +// Send input via NetworkPlugin (recommended) +networkPlugin.sendMoveInput(0, 1); // Movement +networkPlugin.sendActionInput('jump'); // Action -// Handle keyboard input +// Or use inputSystem directly +const inputSystem = networkPlugin.inputSystem; if (keyboard.isPressed('W')) { inputSystem.addMoveInput(0, 1); } @@ -519,74 +595,92 @@ const networkService = services.get(NetworkServiceToken); ### Complete Multiplayer Client ```typescript -import { World, EntitySystem, Matcher } from '@esengine/ecs-framework'; +import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework'; import { NetworkPlugin, NetworkIdentity, - NetworkTransform, - NetworkInputSystem + NetworkTransform } from '@esengine/network'; -// Create game world -const world = new World(); +// Define game scene +class GameScene extends Scene { + initialize(): void { + this.name = 'MultiplayerGame'; + // Network systems are automatically added by NetworkPlugin + // Add custom systems + this.addSystem(new LocalInputHandler()); + } +} -// Configure network plugin -const networkPlugin = new NetworkPlugin({ - serverUrl: 'ws://localhost:3000' -}); -networkPlugin.install(world.services); +// Initialize +async function initGame() { + Core.create({ debug: false }); -// Register player prefab -networkPlugin.registerPrefab('player', (netId, ownerId) => { - const entity = world.createEntity(`player_${netId}`); + const scene = new GameScene(); + Core.setScene(scene); - const identity = new NetworkIdentity(netId, ownerId); - entity.addComponent(identity); - entity.addComponent(new NetworkTransform()); + // Install network plugin + const networkPlugin = new NetworkPlugin(); + await Core.installPlugin(networkPlugin); - // If local player, add input component - if (identity.bIsLocalPlayer) { - entity.addComponent(new LocalInputComponent()); + // Register player prefab + networkPlugin.registerPrefab('player', (scene, spawn) => { + const entity = scene.createEntity(`player_${spawn.netId}`); + + const identity = entity.addComponent(new NetworkIdentity()); + identity.netId = spawn.netId; + identity.ownerId = spawn.ownerId; + identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId; + + entity.addComponent(new NetworkTransform()); + + // If local player, add input marker + if (identity.isLocalPlayer) { + entity.addComponent(new LocalInputComponent()); + } + + return entity; + }); + + // Connect to server + const success = await networkPlugin.connect('ws://localhost:3000', 'Player1'); + if (success) { + console.log('Connected!'); + } else { + console.error('Connection failed'); } - return entity; -}); - -// Connect to server -async function startGame() { - try { - await networkPlugin.connect('Player1'); - console.log('Connected! Player ID:', networkPlugin.localPlayerId); - } catch (error) { - console.error('Connection failed:', error); - } + return networkPlugin; } // Game loop function gameLoop(deltaTime: number) { - world.update(deltaTime); + Core.update(deltaTime); } -startGame(); +initGame(); ``` ### Handling Input ```typescript class LocalInputHandler extends EntitySystem { - private _inputSystem: NetworkInputSystem; + private _networkPlugin: NetworkPlugin | null = null; constructor() { - super(Matcher.all(NetworkIdentity, LocalInputComponent)); + super(Matcher.empty().all(NetworkIdentity, LocalInputComponent)); } - protected onAddedToWorld(): void { - this._inputSystem = this.world.getSystem(NetworkInputSystem); + protected onAddedToScene(): void { + // Get NetworkPlugin reference + this._networkPlugin = Core.getPlugin(NetworkPlugin); } protected processEntity(entity: Entity, dt: number): void { - const identity = entity.getComponent(NetworkIdentity); - if (!identity.bIsLocalPlayer) return; + if (!this._networkPlugin) return; + + const identity = entity.getComponent(NetworkIdentity)!; + if (!identity.isLocalPlayer) return; // Read keyboard input let moveX = 0; @@ -598,11 +692,11 @@ class LocalInputHandler extends EntitySystem { if (keyboard.isPressed('S')) moveY -= 1; if (moveX !== 0 || moveY !== 0) { - this._inputSystem.addMoveInput(moveX, moveY); + this._networkPlugin.sendMoveInput(moveX, moveY); } if (keyboard.isJustPressed('Space')) { - this._inputSystem.addActionInput('jump'); + this._networkPlugin.sendActionInput('jump'); } } } diff --git a/docs/modules/network/index.md b/docs/modules/network/index.md index fab4d76b..b9988a6b 100644 --- a/docs/modules/network/index.md +++ b/docs/modules/network/index.md @@ -22,44 +22,112 @@ npm install @esengine/network npm install @esengine/network-server ``` +## 使用 CLI 快速创建服务端 + +推荐使用 ESEngine CLI 快速创建完整的游戏服务端项目: + +```bash +# 创建项目目录 +mkdir my-game-server && cd my-game-server +npm init -y + +# 使用 CLI 初始化 Node.js 服务端 +npx @esengine/cli init -p nodejs +``` + +CLI 会自动生成以下项目结构: + +``` +my-game-server/ +├── src/ +│ ├── index.ts # 入口文件 +│ ├── server/ +│ │ └── GameServer.ts # 网络服务器配置 +│ └── game/ +│ ├── Game.ts # ECS 游戏主类 +│ ├── scenes/ +│ │ └── MainScene.ts # 主场景 +│ ├── components/ # ECS 组件 +│ │ ├── PositionComponent.ts +│ │ └── VelocityComponent.ts +│ └── systems/ # ECS 系统 +│ └── MovementSystem.ts +├── tsconfig.json +├── package.json +└── README.md +``` + +启动服务端: + +```bash +# 开发模式(热重载) +npm run dev + +# 生产模式 +npm run start +``` + ## 快速开始 ### 客户端 ```typescript -import { World } from '@esengine/ecs-framework'; +import { Core, Scene } from '@esengine/ecs-framework'; import { NetworkPlugin, NetworkIdentity, NetworkTransform } from '@esengine/network'; -// 创建 World 并安装网络插件 -const world = new World(); -const networkPlugin = new NetworkPlugin({ - serverUrl: 'ws://localhost:3000' -}); -networkPlugin.install(world.services); +// 定义游戏场景 +class GameScene extends Scene { + initialize(): void { + this.name = 'Game'; + // 网络系统由 NetworkPlugin 自动添加 + } +} + +// 初始化 Core +Core.create({ debug: false }); +const scene = new GameScene(); +Core.setScene(scene); + +// 安装网络插件 +const networkPlugin = new NetworkPlugin(); +await Core.installPlugin(networkPlugin); // 注册预制体工厂 -networkPlugin.registerPrefab('player', (netId, ownerId) => { - const entity = world.createEntity(`player_${netId}`); - entity.addComponent(new NetworkIdentity(netId, ownerId)); +networkPlugin.registerPrefab('player', (scene, spawn) => { + const entity = scene.createEntity(`player_${spawn.netId}`); + + const identity = entity.addComponent(new NetworkIdentity()); + identity.netId = spawn.netId; + identity.ownerId = spawn.ownerId; + identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId; + entity.addComponent(new NetworkTransform()); - // 添加其他组件... return entity; }); // 连接服务器 -await networkPlugin.connect('PlayerName'); -console.log('Connected! Client ID:', networkPlugin.localPlayerId); +const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName'); +if (success) { + console.log('Connected!'); +} + +// 游戏循环 +function gameLoop(dt: number) { + Core.update(dt); +} // 断开连接 -networkPlugin.disconnect(); +await networkPlugin.disconnect(); ``` ### 服务器端 +使用 CLI 创建服务端项目后,默认生成的代码已经配置好了 GameServer: + ```typescript import { GameServer } from '@esengine/network-server'; @@ -72,6 +140,7 @@ const server = new GameServer({ }); await server.start(); +console.log('Server started on ws://localhost:3000'); ``` ## 核心概念 @@ -229,15 +298,19 @@ interface INetworkCallbacks { ### 预制体工厂 ```typescript -type PrefabFactory = (netId: number, ownerId: number) => Entity; +type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity; ``` 注册预制体工厂用于网络实体的创建: ```typescript -networkPlugin.registerPrefab('enemy', (netId, ownerId) => { - const entity = world.createEntity(`enemy_${netId}`); - entity.addComponent(new NetworkIdentity(netId, ownerId)); +networkPlugin.registerPrefab('enemy', (scene, spawn) => { + const entity = scene.createEntity(`enemy_${spawn.netId}`); + + const identity = entity.addComponent(new NetworkIdentity()); + identity.netId = spawn.netId; + identity.ownerId = spawn.ownerId; + entity.addComponent(new NetworkTransform()); entity.addComponent(new EnemyComponent()); return entity; @@ -264,9 +337,12 @@ class NetworkInputSystem extends EntitySystem { 使用示例: ```typescript -const inputSystem = world.getSystem(NetworkInputSystem); +// 通过 NetworkPlugin 发送输入(推荐) +networkPlugin.sendMoveInput(0, 1); // 移动 +networkPlugin.sendActionInput('jump'); // 动作 -// 处理键盘输入 +// 或直接使用 inputSystem +const inputSystem = networkPlugin.inputSystem; if (keyboard.isPressed('W')) { inputSystem.addMoveInput(0, 1); } @@ -519,74 +595,92 @@ const networkService = services.get(NetworkServiceToken); ### 完整的多人游戏客户端 ```typescript -import { World, EntitySystem, Matcher } from '@esengine/ecs-framework'; +import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework'; import { NetworkPlugin, NetworkIdentity, - NetworkTransform, - NetworkInputSystem + NetworkTransform } from '@esengine/network'; -// 创建游戏世界 -const world = new World(); +// 定义游戏场景 +class GameScene extends Scene { + initialize(): void { + this.name = 'MultiplayerGame'; + // 网络系统由 NetworkPlugin 自动添加 + // 添加自定义系统 + this.addSystem(new LocalInputHandler()); + } +} -// 配置网络插件 -const networkPlugin = new NetworkPlugin({ - serverUrl: 'ws://localhost:3000' -}); -networkPlugin.install(world.services); +// 初始化 +async function initGame() { + Core.create({ debug: false }); -// 注册玩家预制体 -networkPlugin.registerPrefab('player', (netId, ownerId) => { - const entity = world.createEntity(`player_${netId}`); + const scene = new GameScene(); + Core.setScene(scene); - const identity = new NetworkIdentity(netId, ownerId); - entity.addComponent(identity); - entity.addComponent(new NetworkTransform()); + // 安装网络插件 + const networkPlugin = new NetworkPlugin(); + await Core.installPlugin(networkPlugin); - // 如果是本地玩家,添加输入组件 - if (identity.bIsLocalPlayer) { - entity.addComponent(new LocalInputComponent()); + // 注册玩家预制体 + networkPlugin.registerPrefab('player', (scene, spawn) => { + const entity = scene.createEntity(`player_${spawn.netId}`); + + const identity = entity.addComponent(new NetworkIdentity()); + identity.netId = spawn.netId; + identity.ownerId = spawn.ownerId; + identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId; + + entity.addComponent(new NetworkTransform()); + + // 如果是本地玩家,添加输入标记 + if (identity.isLocalPlayer) { + entity.addComponent(new LocalInputComponent()); + } + + return entity; + }); + + // 连接服务器 + const success = await networkPlugin.connect('ws://localhost:3000', 'Player1'); + if (success) { + console.log('已连接!'); + } else { + console.error('连接失败'); } - return entity; -}); - -// 连接服务器 -async function startGame() { - try { - await networkPlugin.connect('Player1'); - console.log('已连接! 玩家 ID:', networkPlugin.localPlayerId); - } catch (error) { - console.error('连接失败:', error); - } + return networkPlugin; } // 游戏循环 function gameLoop(deltaTime: number) { - world.update(deltaTime); + Core.update(deltaTime); } -startGame(); +initGame(); ``` ### 处理输入 ```typescript class LocalInputHandler extends EntitySystem { - private _inputSystem: NetworkInputSystem; + private _networkPlugin: NetworkPlugin | null = null; constructor() { - super(Matcher.all(NetworkIdentity, LocalInputComponent)); + super(Matcher.empty().all(NetworkIdentity, LocalInputComponent)); } - protected onAddedToWorld(): void { - this._inputSystem = this.world.getSystem(NetworkInputSystem); + protected onAddedToScene(): void { + // 获取 NetworkPlugin 引用 + this._networkPlugin = Core.getPlugin(NetworkPlugin); } protected processEntity(entity: Entity, dt: number): void { - const identity = entity.getComponent(NetworkIdentity); - if (!identity.bIsLocalPlayer) return; + if (!this._networkPlugin) return; + + const identity = entity.getComponent(NetworkIdentity)!; + if (!identity.isLocalPlayer) return; // 读取键盘输入 let moveX = 0; @@ -598,11 +692,11 @@ class LocalInputHandler extends EntitySystem { if (keyboard.isPressed('S')) moveY -= 1; if (moveX !== 0 || moveY !== 0) { - this._inputSystem.addMoveInput(moveX, moveY); + this._networkPlugin.sendMoveInput(moveX, moveY); } if (keyboard.isJustPressed('Space')) { - this._inputSystem.addActionInput('jump'); + this._networkPlugin.sendActionInput('jump'); } } } diff --git a/packages/network-ext/network-server/package.json b/packages/network-ext/network-server/package.json index cf879881..8323bfc2 100644 --- a/packages/network-ext/network-server/package.json +++ b/packages/network-ext/network-server/package.json @@ -1,15 +1,21 @@ { "name": "@esengine/network-server", - "version": "1.0.0", + "version": "1.0.1", "description": "TSRPC-based network server for ESEngine", "type": "module", - "main": "dist/index.js", + "main": "dist/index.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, "files": [ @@ -22,7 +28,8 @@ }, "dependencies": { "@esengine/network-protocols": "workspace:*", - "tsrpc": "^3.4.15" + "tsrpc": "^3.4.15", + "ws": "^8.18.0" }, "devDependencies": { "tsup": "^8.5.1", diff --git a/packages/network-ext/network-server/tsup.config.ts b/packages/network-ext/network-server/tsup.config.ts index 22cfecca..6320496e 100644 --- a/packages/network-ext/network-server/tsup.config.ts +++ b/packages/network-ext/network-server/tsup.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/index.ts', 'src/main.ts'], - format: ['esm'], + format: ['esm', 'cjs'], dts: true, sourcemap: true, clean: true, diff --git a/packages/tools/cli/src/adapters/nodejs.ts b/packages/tools/cli/src/adapters/nodejs.ts index 8537e491..5ad1e4f2 100644 --- a/packages/tools/cli/src/adapters/nodejs.ts +++ b/packages/tools/cli/src/adapters/nodejs.ts @@ -7,11 +7,12 @@ import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js'; export const nodejsAdapter: PlatformAdapter = { id: 'nodejs', name: 'Node.js', - description: 'Generate standalone Node.js project with ECS (for servers, CLI tools, simulations)', + description: 'Generate Node.js game server with ECS and networking', getDependencies() { return { - '@esengine/ecs-framework': 'latest' + '@esengine/ecs-framework': 'latest', + '@esengine/network-server': 'latest' }; }, @@ -33,147 +34,96 @@ export const nodejsAdapter: PlatformAdapter = { }, generateFiles(config: ProjectConfig): FileEntry[] { - const files: FileEntry[] = []; - - files.push({ - path: 'src/index.ts', - content: generateIndex(config) - }); - - files.push({ - path: 'src/Game.ts', - content: generateGame(config) - }); - - files.push({ - path: 'src/components/PositionComponent.ts', - content: generatePositionComponent() - }); - - files.push({ - path: 'src/systems/MovementSystem.ts', - content: generateMovementSystem() - }); - - files.push({ - path: 'tsconfig.json', - content: generateTsConfig() - }); - - files.push({ - path: 'README.md', - content: generateReadme(config) - }); - - return files; + return [ + { path: 'src/index.ts', content: generateIndex(config) }, + { path: 'src/server/GameServer.ts', content: generateGameServer(config) }, + { path: 'src/game/Game.ts', content: generateGame(config) }, + { path: 'src/game/scenes/MainScene.ts', content: generateMainScene(config) }, + { path: 'src/game/components/PositionComponent.ts', content: generatePositionComponent() }, + { path: 'src/game/components/VelocityComponent.ts', content: generateVelocityComponent() }, + { path: 'src/game/systems/MovementSystem.ts', content: generateMovementSystem() }, + { path: 'tsconfig.json', content: generateTsConfig() }, + { path: 'README.md', content: generateReadme(config) } + ]; } }; function generateIndex(config: ProjectConfig): string { - return `import { Game } from './Game.js'; + return `import { createGameServer } from './server/GameServer'; -const game = new Game(); +const PORT = Number(process.env.PORT) || 3000; + +async function main() { + const server = createGameServer({ port: PORT }); + await server.start(); + + console.log('========================================'); + console.log(' ${config.name} Server'); + console.log('========================================'); + console.log(\` WebSocket: ws://localhost:\${PORT}\`); + console.log(' Press Ctrl+C to stop'); + console.log('========================================'); +} -// Handle graceful shutdown process.on('SIGINT', () => { console.log('\\nShutting down...'); - game.stop(); process.exit(0); }); -process.on('SIGTERM', () => { - game.stop(); - process.exit(0); -}); +main().catch(console.error); +`; +} -// Start the game -game.start(); +function generateGameServer(config: ProjectConfig): string { + return `import { GameServer, type IServerConfig } from '@esengine/network-server'; +import { Game } from '../game/Game'; -console.log('[${config.name}] Game started. Press Ctrl+C to stop.'); +/** + * @zh 创建游戏服务器 + * @en Create game server + */ +export function createGameServer(config: Partial = {}): GameServer { + const server = new GameServer({ + port: config.port ?? 3000, + roomConfig: { + maxPlayers: 16, + tickRate: 20, + ...config.roomConfig + } + }); + + // 初始化 ECS 游戏逻辑 + const game = new Game(); + game.start(); + + return server; +} `; } function generateGame(config: ProjectConfig): string { - return `import { Core, Scene, type ICoreConfig } from '@esengine/ecs-framework'; -import { MovementSystem } from './systems/MovementSystem.js'; + return `import { Core, type ICoreConfig } from '@esengine/ecs-framework'; +import { MainScene } from './scenes/MainScene'; /** - * Game configuration options - */ -export interface GameOptions { - /** @zh 调试模式 @en Debug mode */ - debug?: boolean; - /** @zh 目标帧率 @en Target FPS */ - targetFPS?: number; - /** @zh 远程调试配置 @en Remote debug configuration */ - remoteDebug?: { - /** @zh 启用远程调试 @en Enable remote debugging */ - enabled: boolean; - /** @zh WebSocket地址 @en WebSocket URL */ - url: string; - /** @zh 自动重连 @en Auto reconnect */ - autoReconnect?: boolean; - }; -} - -/** - * Game Scene - Define your game systems here - */ -class GameScene extends Scene { - initialize(): void { - this.name = '${config.name}'; - this.addSystem(new MovementSystem()); - // Add more systems here... - } - - onStart(): void { - // Create your initial entities here - } -} - -/** - * Main game class with ECS game loop - * - * Features: - * - Configurable debug mode and FPS - * - Remote debugging via WebSocket - * - Fixed timestep game loop - * - Graceful start/stop + * @zh 游戏主类 + * @en Main game class */ export class Game { - private readonly _scene: GameScene; - private readonly _targetFPS: number; + private _scene: MainScene; private _running = false; private _tickInterval: ReturnType | null = null; private _lastTime = 0; + private _targetFPS = 60; - get scene() { return this._scene; } - get running() { return this._running; } - - constructor(options: GameOptions = {}) { - const { debug = false, targetFPS = 60, remoteDebug } = options; + constructor(options: { debug?: boolean; targetFPS?: number } = {}) { + const { debug = false, targetFPS = 60 } = options; this._targetFPS = targetFPS; const config: ICoreConfig = { debug }; - - // 配置远程调试 - if (remoteDebug?.enabled && remoteDebug.url) { - config.debugConfig = { - enabled: true, - websocketUrl: remoteDebug.url, - autoReconnect: remoteDebug.autoReconnect ?? true, - channels: { - entities: true, - systems: true, - performance: true, - components: true, - scenes: true - } - }; - } - Core.create(config); - this._scene = new GameScene(); + + this._scene = new MainScene(); Core.setScene(this._scene); } @@ -203,11 +153,38 @@ export class Game { `; } +function generateMainScene(config: ProjectConfig): string { + return `import { Scene } from '@esengine/ecs-framework'; +import { MovementSystem } from '../systems/MovementSystem'; + +/** + * @zh 主场景 + * @en Main scene + */ +export class MainScene extends Scene { + initialize(): void { + this.name = '${config.name}'; + + // 注册系统 + this.addSystem(new MovementSystem()); + + // 添加更多系统... + } + + onStart(): void { + // 创建初始实体 + console.log('[MainScene] Scene started'); + } +} +`; +} + function generatePositionComponent(): string { return `import { Component, ECSComponent } from '@esengine/ecs-framework'; /** - * Position component - stores entity position + * @zh 位置组件 + * @en Position component */ @ECSComponent('Position') export class PositionComponent extends Component { @@ -219,31 +196,61 @@ export class PositionComponent extends Component { this.x = x; this.y = y; } + + reset(): void { + this.x = 0; + this.y = 0; + } +} +`; +} + +function generateVelocityComponent(): string { + return `import { Component, ECSComponent } from '@esengine/ecs-framework'; + +/** + * @zh 速度组件 + * @en Velocity component + */ +@ECSComponent('Velocity') +export class VelocityComponent extends Component { + vx = 0; + vy = 0; + + constructor(vx = 0, vy = 0) { + super(); + this.vx = vx; + this.vy = vy; + } + + reset(): void { + this.vx = 0; + this.vy = 0; + } } `; } function generateMovementSystem(): string { - return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework'; -import { PositionComponent } from '../components/PositionComponent.js'; + return `import { EntitySystem, Matcher, Entity, Time } from '@esengine/ecs-framework'; +import { PositionComponent } from '../components/PositionComponent'; +import { VelocityComponent } from '../components/VelocityComponent'; /** - * Movement system - processes entities with PositionComponent - * - * Customize this system for your game logic. + * @zh 移动系统 + * @en Movement system */ -@ECSSystem('MovementSystem') export class MovementSystem extends EntitySystem { constructor() { - super(Matcher.empty().all(PositionComponent)); + super(Matcher.empty().all(PositionComponent, VelocityComponent)); } - protected process(entities: readonly Entity[]): void { - for (const entity of entities) { - const position = entity.getComponent(PositionComponent)!; - // Update position using Time.deltaTime - // position.x += velocity.dx * Time.deltaTime; - } + protected processEntity(entity: Entity, dt: number): void { + const pos = entity.getComponent(PositionComponent)!; + const vel = entity.getComponent(VelocityComponent)!; + + pos.x += vel.vx * dt; + pos.y += vel.vy * dt; } } `; @@ -253,8 +260,8 @@ function generateTsConfig(): string { return `{ "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "CommonJS", + "moduleResolution": "Node", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", @@ -263,7 +270,9 @@ function generateTsConfig(): string { "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, - "sourceMap": true + "sourceMap": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] @@ -274,75 +283,54 @@ function generateTsConfig(): string { function generateReadme(config: ProjectConfig): string { return `# ${config.name} -A Node.js project using ESEngine ECS framework. +Node.js 游戏服务器,基于 ESEngine ECS 框架。 -## Quick Start +## 快速开始 \`\`\`bash -# Install dependencies +# 安装依赖 npm install -# Run in development mode (with hot reload) +# 开发模式(热重载) npm run dev -# Build and run +# 构建并运行 npm run build:start \`\`\` -## Project Structure +## 项目结构 \`\`\` src/ -├── index.ts # Entry point -├── Game.ts # Game loop and ECS setup -├── components/ # ECS components (data) -│ └── PositionComponent.ts -└── systems/ # ECS systems (logic) - └── MovementSystem.ts +├── index.ts # 入口文件 +├── server/ +│ └── GameServer.ts # 网络服务器配置 +└── game/ + ├── Game.ts # ECS 游戏主类 + ├── scenes/ + │ └── MainScene.ts # 主场景 + ├── components/ # ECS 组件 + │ ├── PositionComponent.ts + │ └── VelocityComponent.ts + └── systems/ # ECS 系统 + └── MovementSystem.ts \`\`\` -## Creating Components +## 客户端连接 \`\`\`typescript -import { Component } from '@esengine/ecs-framework'; +import { NetworkPlugin } from '@esengine/network'; -export class HealthComponent extends Component { - current = 100; - max = 100; +const networkPlugin = new NetworkPlugin({ + serverUrl: 'ws://localhost:3000' +}); - reset(): void { - this.current = this.max; - } -} +await networkPlugin.connect('PlayerName'); \`\`\` -## Creating Systems +## 文档 -\`\`\`typescript -import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework'; -import { HealthComponent } from '../components/HealthComponent.js'; - -export class HealthSystem extends EntitySystem { - constructor() { - super(Matcher.all(HealthComponent)); - } - - protected processEntity(entity: Entity, dt: number): void { - const health = entity.getComponent(HealthComponent)!; - // Your logic here - } -} -\`\`\` - -## Use Cases - -- Game servers -- CLI tools with complex logic -- Simulations -- Automated testing - -## Documentation - -- [ESEngine ECS Framework](https://github.com/esengine/esengine) +- [ESEngine 文档](https://esengine.github.io/esengine/) +- [Network 模块](https://esengine.github.io/esengine/modules/network/) `; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 905072d1..0171c55d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1705,6 +1705,9 @@ importers: tsrpc: specifier: ^3.4.15 version: 3.4.21 + ws: + specifier: ^8.18.0 + version: 8.18.3 devDependencies: tsup: specifier: ^8.5.1