Compare commits

..

7 Commits

Author SHA1 Message Date
github-actions[bot]
0a88c6f2fc chore: release packages (#355)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-27 00:20:04 +08:00
yhh
b0b95c60b4 fix: 从 ignore 列表移除 network-server 以支持版本发布 2025-12-27 00:17:44 +08:00
yhh
683ac7a7d4 Merge branch 'master' of https://github.com/esengine/esengine 2025-12-27 00:16:12 +08:00
YHH
1e240e86f2 feat(cli): 增强 Node.js 服务端适配器 (#354)
* docs(network): 添加网络模块文档和 CLI 支持

- 添加中英文网络模块文档
- 将 network、network-protocols、network-server 加入 CLI 模块列表

* feat(cli): 增强 Node.js 服务端适配器

- 添加 @esengine/network-server 依赖支持
- 生成完整的 ECS 游戏服务器项目结构
- 修复 network-server 包支持 ESM/CJS 双格式
- 添加 ws@8.18.0 解决 Node.js 24 兼容性问题
- 组件使用 @ECSComponent 装饰器注册
- tsconfig 启用 experimentalDecorators
2025-12-27 00:13:58 +08:00
yhh
4d6c2fe7ff Merge branch 'master' of https://github.com/esengine/esengine 2025-12-26 23:21:17 +08:00
yhh
a42f2412d7 Merge branch 'master' of https://github.com/esengine/esengine 2025-12-26 23:04:47 +08:00
yhh
fdb19a33fb docs(network): 添加网络模块文档和 CLI 支持
- 添加中英文网络模块文档
- 将 network、network-protocols、network-server 加入 CLI 模块列表
2025-12-26 23:01:07 +08:00
10 changed files with 525 additions and 307 deletions

View File

@@ -33,7 +33,6 @@
"@esengine/physics-rapier2d", "@esengine/physics-rapier2d",
"@esengine/rapier2d", "@esengine/rapier2d",
"@esengine/world-streaming", "@esengine/world-streaming",
"@esengine/network-server",
"@esengine/editor-core", "@esengine/editor-core",
"@esengine/editor-runtime", "@esengine/editor-runtime",
"@esengine/editor-app", "@esengine/editor-app",

View File

@@ -22,44 +22,112 @@ npm install @esengine/network
npm install @esengine/network-server 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 ## Quick Start
### Client ### Client
```typescript ```typescript
import { World } from '@esengine/ecs-framework'; import { Core, Scene } from '@esengine/ecs-framework';
import { import {
NetworkPlugin, NetworkPlugin,
NetworkIdentity, NetworkIdentity,
NetworkTransform NetworkTransform
} from '@esengine/network'; } from '@esengine/network';
// Create World and install network plugin // Define game scene
const world = new World(); class GameScene extends Scene {
const networkPlugin = new NetworkPlugin({ initialize(): void {
serverUrl: 'ws://localhost:3000' this.name = 'Game';
}); // Network systems are automatically added by NetworkPlugin
networkPlugin.install(world.services); }
}
// 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 // Register prefab factory
networkPlugin.registerPrefab('player', (netId, ownerId) => { networkPlugin.registerPrefab('player', (scene, spawn) => {
const entity = world.createEntity(`player_${netId}`); const entity = scene.createEntity(`player_${spawn.netId}`);
entity.addComponent(new NetworkIdentity(netId, ownerId));
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()); entity.addComponent(new NetworkTransform());
// Add other components...
return entity; return entity;
}); });
// Connect to server // Connect to server
await networkPlugin.connect('PlayerName'); const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
console.log('Connected! Client ID:', networkPlugin.localPlayerId); if (success) {
console.log('Connected!');
}
// Game loop
function gameLoop(dt: number) {
Core.update(dt);
}
// Disconnect // Disconnect
networkPlugin.disconnect(); await networkPlugin.disconnect();
``` ```
### Server ### Server
After creating a server project with CLI, the generated code already configures GameServer:
```typescript ```typescript
import { GameServer } from '@esengine/network-server'; import { GameServer } from '@esengine/network-server';
@@ -72,6 +140,7 @@ const server = new GameServer({
}); });
await server.start(); await server.start();
console.log('Server started on ws://localhost:3000');
``` ```
## Core Concepts ## Core Concepts
@@ -229,15 +298,19 @@ interface INetworkCallbacks {
### Prefab Factory ### Prefab Factory
```typescript ```typescript
type PrefabFactory = (netId: number, ownerId: number) => Entity; type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
``` ```
Register prefab factories for network entity creation: Register prefab factories for network entity creation:
```typescript ```typescript
networkPlugin.registerPrefab('enemy', (netId, ownerId) => { networkPlugin.registerPrefab('enemy', (scene, spawn) => {
const entity = world.createEntity(`enemy_${netId}`); const entity = scene.createEntity(`enemy_${spawn.netId}`);
entity.addComponent(new NetworkIdentity(netId, ownerId));
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
entity.addComponent(new NetworkTransform()); entity.addComponent(new NetworkTransform());
entity.addComponent(new EnemyComponent()); entity.addComponent(new EnemyComponent());
return entity; return entity;
@@ -264,9 +337,12 @@ class NetworkInputSystem extends EntitySystem {
Usage example: Usage example:
```typescript ```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')) { if (keyboard.isPressed('W')) {
inputSystem.addMoveInput(0, 1); inputSystem.addMoveInput(0, 1);
} }
@@ -519,74 +595,92 @@ const networkService = services.get(NetworkServiceToken);
### Complete Multiplayer Client ### Complete Multiplayer Client
```typescript ```typescript
import { World, EntitySystem, Matcher } from '@esengine/ecs-framework'; import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
import { import {
NetworkPlugin, NetworkPlugin,
NetworkIdentity, NetworkIdentity,
NetworkTransform, NetworkTransform
NetworkInputSystem
} from '@esengine/network'; } from '@esengine/network';
// Create game world // Define game scene
const world = new World(); 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 // Initialize
const networkPlugin = new NetworkPlugin({ async function initGame() {
serverUrl: 'ws://localhost:3000' Core.create({ debug: false });
});
networkPlugin.install(world.services);
// Register player prefab const scene = new GameScene();
networkPlugin.registerPrefab('player', (netId, ownerId) => { Core.setScene(scene);
const entity = world.createEntity(`player_${netId}`);
const identity = new NetworkIdentity(netId, ownerId); // Install network plugin
entity.addComponent(identity); const networkPlugin = new NetworkPlugin();
entity.addComponent(new NetworkTransform()); await Core.installPlugin(networkPlugin);
// If local player, add input component // Register player prefab
if (identity.bIsLocalPlayer) { networkPlugin.registerPrefab('player', (scene, spawn) => {
entity.addComponent(new LocalInputComponent()); 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; return networkPlugin;
});
// 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);
}
} }
// Game loop // Game loop
function gameLoop(deltaTime: number) { function gameLoop(deltaTime: number) {
world.update(deltaTime); Core.update(deltaTime);
} }
startGame(); initGame();
``` ```
### Handling Input ### Handling Input
```typescript ```typescript
class LocalInputHandler extends EntitySystem { class LocalInputHandler extends EntitySystem {
private _inputSystem: NetworkInputSystem; private _networkPlugin: NetworkPlugin | null = null;
constructor() { constructor() {
super(Matcher.all(NetworkIdentity, LocalInputComponent)); super(Matcher.empty().all(NetworkIdentity, LocalInputComponent));
} }
protected onAddedToWorld(): void { protected onAddedToScene(): void {
this._inputSystem = this.world.getSystem(NetworkInputSystem); // Get NetworkPlugin reference
this._networkPlugin = Core.getPlugin(NetworkPlugin);
} }
protected processEntity(entity: Entity, dt: number): void { protected processEntity(entity: Entity, dt: number): void {
const identity = entity.getComponent(NetworkIdentity); if (!this._networkPlugin) return;
if (!identity.bIsLocalPlayer) return;
const identity = entity.getComponent(NetworkIdentity)!;
if (!identity.isLocalPlayer) return;
// Read keyboard input // Read keyboard input
let moveX = 0; let moveX = 0;
@@ -598,11 +692,11 @@ class LocalInputHandler extends EntitySystem {
if (keyboard.isPressed('S')) moveY -= 1; if (keyboard.isPressed('S')) moveY -= 1;
if (moveX !== 0 || moveY !== 0) { if (moveX !== 0 || moveY !== 0) {
this._inputSystem.addMoveInput(moveX, moveY); this._networkPlugin.sendMoveInput(moveX, moveY);
} }
if (keyboard.isJustPressed('Space')) { if (keyboard.isJustPressed('Space')) {
this._inputSystem.addActionInput('jump'); this._networkPlugin.sendActionInput('jump');
} }
} }
} }

View File

@@ -22,44 +22,112 @@ npm install @esengine/network
npm install @esengine/network-server 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 ```typescript
import { World } from '@esengine/ecs-framework'; import { Core, Scene } from '@esengine/ecs-framework';
import { import {
NetworkPlugin, NetworkPlugin,
NetworkIdentity, NetworkIdentity,
NetworkTransform NetworkTransform
} from '@esengine/network'; } from '@esengine/network';
// 创建 World 并安装网络插件 // 定义游戏场景
const world = new World(); class GameScene extends Scene {
const networkPlugin = new NetworkPlugin({ initialize(): void {
serverUrl: 'ws://localhost:3000' this.name = 'Game';
}); // 网络系统由 NetworkPlugin 自动添加
networkPlugin.install(world.services); }
}
// 初始化 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) => { networkPlugin.registerPrefab('player', (scene, spawn) => {
const entity = world.createEntity(`player_${netId}`); const entity = scene.createEntity(`player_${spawn.netId}`);
entity.addComponent(new NetworkIdentity(netId, ownerId));
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()); entity.addComponent(new NetworkTransform());
// 添加其他组件...
return entity; return entity;
}); });
// 连接服务器 // 连接服务器
await networkPlugin.connect('PlayerName'); const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
console.log('Connected! Client ID:', networkPlugin.localPlayerId); if (success) {
console.log('Connected!');
}
// 游戏循环
function gameLoop(dt: number) {
Core.update(dt);
}
// 断开连接 // 断开连接
networkPlugin.disconnect(); await networkPlugin.disconnect();
``` ```
### 服务器端 ### 服务器端
使用 CLI 创建服务端项目后,默认生成的代码已经配置好了 GameServer
```typescript ```typescript
import { GameServer } from '@esengine/network-server'; import { GameServer } from '@esengine/network-server';
@@ -72,6 +140,7 @@ const server = new GameServer({
}); });
await server.start(); await server.start();
console.log('Server started on ws://localhost:3000');
``` ```
## 核心概念 ## 核心概念
@@ -229,15 +298,19 @@ interface INetworkCallbacks {
### 预制体工厂 ### 预制体工厂
```typescript ```typescript
type PrefabFactory = (netId: number, ownerId: number) => Entity; type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
``` ```
注册预制体工厂用于网络实体的创建: 注册预制体工厂用于网络实体的创建:
```typescript ```typescript
networkPlugin.registerPrefab('enemy', (netId, ownerId) => { networkPlugin.registerPrefab('enemy', (scene, spawn) => {
const entity = world.createEntity(`enemy_${netId}`); const entity = scene.createEntity(`enemy_${spawn.netId}`);
entity.addComponent(new NetworkIdentity(netId, ownerId));
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
entity.addComponent(new NetworkTransform()); entity.addComponent(new NetworkTransform());
entity.addComponent(new EnemyComponent()); entity.addComponent(new EnemyComponent());
return entity; return entity;
@@ -264,9 +337,12 @@ class NetworkInputSystem extends EntitySystem {
使用示例: 使用示例:
```typescript ```typescript
const inputSystem = world.getSystem(NetworkInputSystem); // 通过 NetworkPlugin 发送输入(推荐)
networkPlugin.sendMoveInput(0, 1); // 移动
networkPlugin.sendActionInput('jump'); // 动作
// 处理键盘输入 // 或直接使用 inputSystem
const inputSystem = networkPlugin.inputSystem;
if (keyboard.isPressed('W')) { if (keyboard.isPressed('W')) {
inputSystem.addMoveInput(0, 1); inputSystem.addMoveInput(0, 1);
} }
@@ -519,74 +595,92 @@ const networkService = services.get(NetworkServiceToken);
### 完整的多人游戏客户端 ### 完整的多人游戏客户端
```typescript ```typescript
import { World, EntitySystem, Matcher } from '@esengine/ecs-framework'; import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
import { import {
NetworkPlugin, NetworkPlugin,
NetworkIdentity, NetworkIdentity,
NetworkTransform, NetworkTransform
NetworkInputSystem
} from '@esengine/network'; } 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({ async function initGame() {
serverUrl: 'ws://localhost:3000' Core.create({ debug: false });
});
networkPlugin.install(world.services);
// 注册玩家预制体 const scene = new GameScene();
networkPlugin.registerPrefab('player', (netId, ownerId) => { Core.setScene(scene);
const entity = world.createEntity(`player_${netId}`);
const identity = new NetworkIdentity(netId, ownerId); // 安装网络插件
entity.addComponent(identity); const networkPlugin = new NetworkPlugin();
entity.addComponent(new NetworkTransform()); await Core.installPlugin(networkPlugin);
// 如果是本地玩家,添加输入组件 // 注册玩家预制体
if (identity.bIsLocalPlayer) { networkPlugin.registerPrefab('player', (scene, spawn) => {
entity.addComponent(new LocalInputComponent()); 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; return networkPlugin;
});
// 连接服务器
async function startGame() {
try {
await networkPlugin.connect('Player1');
console.log('已连接! 玩家 ID:', networkPlugin.localPlayerId);
} catch (error) {
console.error('连接失败:', error);
}
} }
// 游戏循环 // 游戏循环
function gameLoop(deltaTime: number) { function gameLoop(deltaTime: number) {
world.update(deltaTime); Core.update(deltaTime);
} }
startGame(); initGame();
``` ```
### 处理输入 ### 处理输入
```typescript ```typescript
class LocalInputHandler extends EntitySystem { class LocalInputHandler extends EntitySystem {
private _inputSystem: NetworkInputSystem; private _networkPlugin: NetworkPlugin | null = null;
constructor() { constructor() {
super(Matcher.all(NetworkIdentity, LocalInputComponent)); super(Matcher.empty().all(NetworkIdentity, LocalInputComponent));
} }
protected onAddedToWorld(): void { protected onAddedToScene(): void {
this._inputSystem = this.world.getSystem(NetworkInputSystem); // 获取 NetworkPlugin 引用
this._networkPlugin = Core.getPlugin(NetworkPlugin);
} }
protected processEntity(entity: Entity, dt: number): void { protected processEntity(entity: Entity, dt: number): void {
const identity = entity.getComponent(NetworkIdentity); if (!this._networkPlugin) return;
if (!identity.bIsLocalPlayer) return;
const identity = entity.getComponent(NetworkIdentity)!;
if (!identity.isLocalPlayer) return;
// 读取键盘输入 // 读取键盘输入
let moveX = 0; let moveX = 0;
@@ -598,11 +692,11 @@ class LocalInputHandler extends EntitySystem {
if (keyboard.isPressed('S')) moveY -= 1; if (keyboard.isPressed('S')) moveY -= 1;
if (moveX !== 0 || moveY !== 0) { if (moveX !== 0 || moveY !== 0) {
this._inputSystem.addMoveInput(moveX, moveY); this._networkPlugin.sendMoveInput(moveX, moveY);
} }
if (keyboard.isJustPressed('Space')) { if (keyboard.isJustPressed('Space')) {
this._inputSystem.addActionInput('jump'); this._networkPlugin.sendActionInput('jump');
} }
} }
} }

View File

@@ -0,0 +1,17 @@
# @esengine/network-server
## 1.0.2
### Patch Changes
- [#354](https://github.com/esengine/esengine/pull/354) [`1e240e8`](https://github.com/esengine/esengine/commit/1e240e86f2f75672c3609c9d86238a9ec08ebb4e) Thanks [@esengine](https://github.com/esengine)! - 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 兼容性问题

View File

@@ -1,15 +1,21 @@
{ {
"name": "@esengine/network-server", "name": "@esengine/network-server",
"version": "1.0.0", "version": "1.0.2",
"description": "TSRPC-based network server for ESEngine", "description": "TSRPC-based network server for ESEngine",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.cjs",
"module": "dist/index.js", "module": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {
".": { ".": {
"import": "./dist/index.js", "import": {
"types": "./dist/index.d.ts" "types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
} }
}, },
"files": [ "files": [
@@ -22,7 +28,8 @@
}, },
"dependencies": { "dependencies": {
"@esengine/network-protocols": "workspace:*", "@esengine/network-protocols": "workspace:*",
"tsrpc": "^3.4.15" "tsrpc": "^3.4.15",
"ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"tsup": "^8.5.1", "tsup": "^8.5.1",

View File

@@ -2,7 +2,7 @@ import { defineConfig } from 'tsup';
export default defineConfig({ export default defineConfig({
entry: ['src/index.ts', 'src/main.ts'], entry: ['src/index.ts', 'src/main.ts'],
format: ['esm'], format: ['esm', 'cjs'],
dts: true, dts: true,
sourcemap: true, sourcemap: true,
clean: true, clean: true,

View File

@@ -1,5 +1,21 @@
# @esengine/cli # @esengine/cli
## 1.3.0
### Minor Changes
- [#354](https://github.com/esengine/esengine/pull/354) [`1e240e8`](https://github.com/esengine/esengine/commit/1e240e86f2f75672c3609c9d86238a9ec08ebb4e) Thanks [@esengine](https://github.com/esengine)! - 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 兼容性问题
## 1.2.1 ## 1.2.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@esengine/cli", "name": "@esengine/cli",
"version": "1.2.1", "version": "1.3.0",
"description": "CLI tool for adding ESEngine ECS framework to existing projects", "description": "CLI tool for adding ESEngine ECS framework to existing projects",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -7,11 +7,12 @@ import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
export const nodejsAdapter: PlatformAdapter = { export const nodejsAdapter: PlatformAdapter = {
id: 'nodejs', id: 'nodejs',
name: 'Node.js', 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() { getDependencies() {
return { 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[] { generateFiles(config: ProjectConfig): FileEntry[] {
const files: FileEntry[] = []; return [
{ path: 'src/index.ts', content: generateIndex(config) },
files.push({ { path: 'src/server/GameServer.ts', content: generateGameServer(config) },
path: 'src/index.ts', { path: 'src/game/Game.ts', content: generateGame(config) },
content: generateIndex(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() },
files.push({ { path: 'src/game/systems/MovementSystem.ts', content: generateMovementSystem() },
path: 'src/Game.ts', { path: 'tsconfig.json', content: generateTsConfig() },
content: generateGame(config) { path: 'README.md', content: generateReadme(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;
} }
}; };
function generateIndex(config: ProjectConfig): string { 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', () => { process.on('SIGINT', () => {
console.log('\\nShutting down...'); console.log('\\nShutting down...');
game.stop();
process.exit(0); process.exit(0);
}); });
process.on('SIGTERM', () => { main().catch(console.error);
game.stop(); `;
process.exit(0); }
});
// Start the game function generateGameServer(config: ProjectConfig): string {
game.start(); 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<IServerConfig> = {}): 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 { function generateGame(config: ProjectConfig): string {
return `import { Core, Scene, type ICoreConfig } from '@esengine/ecs-framework'; return `import { Core, type ICoreConfig } from '@esengine/ecs-framework';
import { MovementSystem } from './systems/MovementSystem.js'; import { MainScene } from './scenes/MainScene';
/** /**
* Game configuration options * @zh 游戏主类
*/ * @en Main game class
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
*/ */
export class Game { export class Game {
private readonly _scene: GameScene; private _scene: MainScene;
private readonly _targetFPS: number;
private _running = false; private _running = false;
private _tickInterval: ReturnType<typeof setInterval> | null = null; private _tickInterval: ReturnType<typeof setInterval> | null = null;
private _lastTime = 0; private _lastTime = 0;
private _targetFPS = 60;
get scene() { return this._scene; } constructor(options: { debug?: boolean; targetFPS?: number } = {}) {
get running() { return this._running; } const { debug = false, targetFPS = 60 } = options;
constructor(options: GameOptions = {}) {
const { debug = false, targetFPS = 60, remoteDebug } = options;
this._targetFPS = targetFPS; this._targetFPS = targetFPS;
const config: ICoreConfig = { debug }; 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); Core.create(config);
this._scene = new GameScene();
this._scene = new MainScene();
Core.setScene(this._scene); 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 { function generatePositionComponent(): string {
return `import { Component, ECSComponent } from '@esengine/ecs-framework'; return `import { Component, ECSComponent } from '@esengine/ecs-framework';
/** /**
* Position component - stores entity position * @zh 位置组件
* @en Position component
*/ */
@ECSComponent('Position') @ECSComponent('Position')
export class PositionComponent extends Component { export class PositionComponent extends Component {
@@ -219,31 +196,61 @@ export class PositionComponent extends Component {
this.x = x; this.x = x;
this.y = y; 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 { function generateMovementSystem(): string {
return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework'; return `import { EntitySystem, Matcher, Entity, Time } from '@esengine/ecs-framework';
import { PositionComponent } from '../components/PositionComponent.js'; import { PositionComponent } from '../components/PositionComponent';
import { VelocityComponent } from '../components/VelocityComponent';
/** /**
* Movement system - processes entities with PositionComponent * @zh 移动系统
* * @en Movement system
* Customize this system for your game logic.
*/ */
@ECSSystem('MovementSystem')
export class MovementSystem extends EntitySystem { export class MovementSystem extends EntitySystem {
constructor() { constructor() {
super(Matcher.empty().all(PositionComponent)); super(Matcher.empty().all(PositionComponent, VelocityComponent));
} }
protected process(entities: readonly Entity[]): void { protected processEntity(entity: Entity, dt: number): void {
for (const entity of entities) { const pos = entity.getComponent(PositionComponent)!;
const position = entity.getComponent(PositionComponent)!; const vel = entity.getComponent(VelocityComponent)!;
// Update position using Time.deltaTime
// position.x += velocity.dx * Time.deltaTime; pos.x += vel.vx * dt;
} pos.y += vel.vy * dt;
} }
} }
`; `;
@@ -253,8 +260,8 @@ function generateTsConfig(): string {
return `{ return `{
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "CommonJS",
"moduleResolution": "NodeNext", "moduleResolution": "Node",
"lib": ["ES2022"], "lib": ["ES2022"],
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
@@ -263,7 +270,9 @@ function generateTsConfig(): string {
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"declaration": true, "declaration": true,
"sourceMap": true "sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
@@ -274,75 +283,54 @@ function generateTsConfig(): string {
function generateReadme(config: ProjectConfig): string { function generateReadme(config: ProjectConfig): string {
return `# ${config.name} return `# ${config.name}
A Node.js project using ESEngine ECS framework. Node.js 游戏服务器,基于 ESEngine ECS 框架。
## Quick Start ## 快速开始
\`\`\`bash \`\`\`bash
# Install dependencies # 安装依赖
npm install npm install
# Run in development mode (with hot reload) # 开发模式(热重载)
npm run dev npm run dev
# Build and run # 构建并运行
npm run build:start npm run build:start
\`\`\` \`\`\`
## Project Structure ## 项目结构
\`\`\` \`\`\`
src/ src/
├── index.ts # Entry point ├── index.ts # 入口文件
├── Game.ts # Game loop and ECS setup ├── server/
├── components/ # ECS components (data) │ └── GameServer.ts # 网络服务器配置
│ └── PositionComponent.ts └── game/
└── systems/ # ECS systems (logic) ├── Game.ts # ECS 游戏主类
── MovementSystem.ts ── scenes/
│ └── MainScene.ts # 主场景
├── components/ # ECS 组件
│ ├── PositionComponent.ts
│ └── VelocityComponent.ts
└── systems/ # ECS 系统
└── MovementSystem.ts
\`\`\` \`\`\`
## Creating Components ## 客户端连接
\`\`\`typescript \`\`\`typescript
import { Component } from '@esengine/ecs-framework'; import { NetworkPlugin } from '@esengine/network';
export class HealthComponent extends Component { const networkPlugin = new NetworkPlugin({
current = 100; serverUrl: 'ws://localhost:3000'
max = 100; });
reset(): void { await networkPlugin.connect('PlayerName');
this.current = this.max;
}
}
\`\`\` \`\`\`
## Creating Systems ## 文档
\`\`\`typescript - [ESEngine 文档](https://esengine.github.io/esengine/)
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework'; - [Network 模块](https://esengine.github.io/esengine/modules/network/)
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)
`; `;
} }

3
pnpm-lock.yaml generated
View File

@@ -1705,6 +1705,9 @@ importers:
tsrpc: tsrpc:
specifier: ^3.4.15 specifier: ^3.4.15
version: 3.4.21 version: 3.4.21
ws:
specifier: ^8.18.0
version: 8.18.3
devDependencies: devDependencies:
tsup: tsup:
specifier: ^8.5.1 specifier: ^8.5.1