diff --git a/.changeset/cli-cocos-detection-fix.md b/.changeset/cli-cocos-detection-fix.md new file mode 100644 index 00000000..40564167 --- /dev/null +++ b/.changeset/cli-cocos-detection-fix.md @@ -0,0 +1,9 @@ +--- +"@esengine/cli": patch +--- + +fix(cli): 修复 Cocos Creator 3.x 项目检测逻辑 + +- 优先检查 package.json 中的 creator.version 字段 +- 添加 .creator 和 settings 目录检测 +- 重构检测代码,提取通用辅助函数 diff --git a/docs/en/modules/network/index.md b/docs/en/modules/network/index.md new file mode 100644 index 00000000..ff76d878 --- /dev/null +++ b/docs/en/modules/network/index.md @@ -0,0 +1,633 @@ +# Network System + +`@esengine/network` provides a TSRPC-based client-server network synchronization solution for multiplayer games, including entity synchronization, input handling, and state interpolation. + +## Overview + +The network module consists of three packages: + +| Package | Description | +|---------|-------------| +| `@esengine/network` | Client-side ECS plugin | +| `@esengine/network-protocols` | Shared protocol definitions | +| `@esengine/network-server` | Server-side implementation | + +## Installation + +```bash +# Client +npm install @esengine/network + +# Server +npm install @esengine/network-server +``` + +## Quick Start + +### Client + +```typescript +import { World } 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); + +// Register prefab factory +networkPlugin.registerPrefab('player', (netId, ownerId) => { + const entity = world.createEntity(`player_${netId}`); + entity.addComponent(new NetworkIdentity(netId, ownerId)); + entity.addComponent(new NetworkTransform()); + // Add other components... + return entity; +}); + +// Connect to server +await networkPlugin.connect('PlayerName'); +console.log('Connected! Client ID:', networkPlugin.localPlayerId); + +// Disconnect +networkPlugin.disconnect(); +``` + +### Server + +```typescript +import { GameServer } from '@esengine/network-server'; + +const server = new GameServer({ + port: 3000, + roomConfig: { + maxPlayers: 16, + tickRate: 20 + } +}); + +await server.start(); +``` + +## Core Concepts + +### Architecture + +``` +Client Server +┌────────────────┐ ┌────────────────┐ +│ NetworkPlugin │◄──── WS ────► │ GameServer │ +│ ├─ Service │ │ ├─ Room │ +│ ├─ SyncSystem │ │ └─ Players │ +│ ├─ SpawnSystem │ └────────────────┘ +│ └─ InputSystem │ +└────────────────┘ +``` + +### Components + +#### NetworkIdentity + +Network identity component, required for every networked entity: + +```typescript +class NetworkIdentity extends Component { + netId: number; // Network unique ID + ownerId: number; // Owner client ID + bIsLocalPlayer: boolean; // Whether local player + bHasAuthority: boolean; // Whether has control authority +} +``` + +#### NetworkTransform + +Network transform component for position and rotation sync: + +```typescript +class NetworkTransform extends Component { + position: { x: number; y: number }; + rotation: number; + velocity: { x: number; y: number }; +} +``` + +### Systems + +#### NetworkSyncSystem + +Handles server state synchronization and interpolation: + +- Receives server state snapshots +- Stores states in snapshot buffer +- Performs interpolation for remote entities + +#### NetworkSpawnSystem + +Handles network entity spawning and despawning: + +- Listens for Spawn/Despawn messages +- Creates entities using registered prefab factories +- Manages networked entity lifecycle + +#### NetworkInputSystem + +Handles local player input sending: + +- Collects local player input +- Sends input to server +- Supports movement and action inputs + +## API Reference + +### NetworkPlugin + +```typescript +class NetworkPlugin { + constructor(config: INetworkPluginConfig); + + // Install plugin + install(services: ServiceContainer): void; + + // Connect to server + connect(playerName: string, roomId?: string): Promise; + + // Disconnect + disconnect(): void; + + // Register prefab factory + registerPrefab(prefab: string, factory: PrefabFactory): void; + + // Properties + readonly localPlayerId: number | null; + readonly isConnected: boolean; +} +``` + +**Configuration:** + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `serverUrl` | `string` | Yes | WebSocket server URL | + +### NetworkService + +Network service managing WebSocket connections: + +```typescript +class NetworkService { + // Connection state + readonly state: ENetworkState; + readonly isConnected: boolean; + readonly clientId: number | null; + readonly roomId: string | null; + + // Connection control + connect(serverUrl: string): Promise; + disconnect(): void; + + // Join room + join(playerName: string, roomId?: string): Promise; + + // Send input + sendInput(input: IPlayerInput): void; + + // Event callbacks + setCallbacks(callbacks: Partial): void; +} +``` + +**Network state enum:** + +```typescript +enum ENetworkState { + Disconnected = 'disconnected', + Connecting = 'connecting', + Connected = 'connected', + Joining = 'joining', + Joined = 'joined' +} +``` + +**Callbacks interface:** + +```typescript +interface INetworkCallbacks { + onConnected?: () => void; + onDisconnected?: () => void; + onJoined?: (clientId: number, roomId: string) => void; + onSync?: (msg: MsgSync) => void; + onSpawn?: (msg: MsgSpawn) => void; + onDespawn?: (msg: MsgDespawn) => void; +} +``` + +### Prefab Factory + +```typescript +type PrefabFactory = (netId: number, ownerId: number) => 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)); + entity.addComponent(new NetworkTransform()); + entity.addComponent(new EnemyComponent()); + return entity; +}); +``` + +### Input System + +#### NetworkInputSystem + +```typescript +class NetworkInputSystem extends EntitySystem { + // Add movement input + addMoveInput(x: number, y: number): void; + + // Add action input + addActionInput(action: string): void; + + // Clear input + clearInput(): void; +} +``` + +Usage example: + +```typescript +const inputSystem = world.getSystem(NetworkInputSystem); + +// Handle keyboard input +if (keyboard.isPressed('W')) { + inputSystem.addMoveInput(0, 1); +} +if (keyboard.isPressed('Space')) { + inputSystem.addActionInput('jump'); +} +``` + +## State Synchronization + +### Snapshot Buffer + +Stores server state snapshots for interpolation: + +```typescript +import { createSnapshotBuffer, type IStateSnapshot } from '@esengine/network'; + +const buffer = createSnapshotBuffer({ + maxSnapshots: 30, // Max snapshots + interpolationDelay: 100 // Interpolation delay (ms) +}); + +// Add snapshot +buffer.addSnapshot({ + time: serverTime, + entities: states +}); + +// Get interpolated state +const interpolated = buffer.getInterpolatedState(clientTime); +``` + +### Transform Interpolators + +#### Linear Interpolator + +```typescript +import { createTransformInterpolator } from '@esengine/network'; + +const interpolator = createTransformInterpolator(); + +// Add state +interpolator.addState(time, { x: 0, y: 0, rotation: 0 }); + +// Get interpolated result +const state = interpolator.getInterpolatedState(currentTime); +``` + +#### Hermite Interpolator + +Uses Hermite splines for smoother interpolation: + +```typescript +import { createHermiteTransformInterpolator } from '@esengine/network'; + +const interpolator = createHermiteTransformInterpolator({ + bufferSize: 10 +}); + +// Add state with velocity +interpolator.addState(time, { + x: 100, + y: 200, + rotation: 0, + vx: 5, + vy: 0 +}); + +// Get smooth interpolated result +const state = interpolator.getInterpolatedState(currentTime); +``` + +### Client Prediction + +Implement client-side prediction with server reconciliation: + +```typescript +import { createClientPrediction } from '@esengine/network'; + +const prediction = createClientPrediction({ + maxPredictedInputs: 60, + reconciliationThreshold: 0.1 +}); + +// Predict input +const seq = prediction.predict(inputState, currentState, (state, input) => { + // Apply input to state + return applyInput(state, input); +}); + +// Server reconciliation +const corrected = prediction.reconcile( + serverState, + serverSeq, + (state, input) => applyInput(state, input) +); +``` + +## Server Side + +### GameServer + +```typescript +import { GameServer } from '@esengine/network-server'; + +const server = new GameServer({ + port: 3000, + roomConfig: { + maxPlayers: 16, // Max players per room + tickRate: 20 // Sync rate (Hz) + } +}); + +// Start server +await server.start(); + +// Get room +const room = server.getOrCreateRoom('room-id'); + +// Stop server +await server.stop(); +``` + +### Room + +```typescript +class Room { + readonly id: string; + readonly playerCount: number; + readonly isFull: boolean; + + // Add player + addPlayer(name: string, connection: Connection): IPlayer | null; + + // Remove player + removePlayer(clientId: number): void; + + // Get player + getPlayer(clientId: number): IPlayer | undefined; + + // Handle input + handleInput(clientId: number, input: IPlayerInput): void; + + // Destroy room + destroy(): void; +} +``` + +**Player interface:** + +```typescript +interface IPlayer { + clientId: number; // Client ID + name: string; // Player name + connection: Connection; // Connection object + netId: number; // Network entity ID +} +``` + +## Protocol Types + +### Message Types + +```typescript +// State sync message +interface MsgSync { + time: number; + entities: IEntityState[]; +} + +// Entity state +interface IEntityState { + netId: number; + pos?: Vec2; + rot?: number; +} + +// Spawn message +interface MsgSpawn { + netId: number; + ownerId: number; + prefab: string; + pos: Vec2; + rot: number; +} + +// Despawn message +interface MsgDespawn { + netId: number; +} + +// Input message +interface MsgInput { + input: IPlayerInput; +} + +// Player input +interface IPlayerInput { + seq?: number; + moveDir?: Vec2; + actions?: string[]; +} +``` + +### API Types + +```typescript +// Join request +interface ReqJoin { + playerName: string; + roomId?: string; +} + +// Join response +interface ResJoin { + clientId: number; + roomId: string; + playerCount: number; +} +``` + +## Blueprint Nodes + +The network module provides blueprint nodes for visual scripting: + +- `IsLocalPlayer` - Check if entity is local player +- `IsServer` - Check if running on server +- `HasAuthority` - Check if has authority over entity +- `GetNetworkId` - Get entity's network ID +- `GetLocalPlayerId` - Get local player ID + +## Service Tokens + +For dependency injection: + +```typescript +import { + NetworkServiceToken, + NetworkSyncSystemToken, + NetworkSpawnSystemToken, + NetworkInputSystemToken +} from '@esengine/network'; + +// Get service +const networkService = services.get(NetworkServiceToken); +``` + +## Practical Example + +### Complete Multiplayer Client + +```typescript +import { World, EntitySystem, Matcher } from '@esengine/ecs-framework'; +import { + NetworkPlugin, + NetworkIdentity, + NetworkTransform, + NetworkInputSystem +} from '@esengine/network'; + +// Create game world +const world = new World(); + +// Configure network plugin +const networkPlugin = new NetworkPlugin({ + serverUrl: 'ws://localhost:3000' +}); +networkPlugin.install(world.services); + +// Register player prefab +networkPlugin.registerPrefab('player', (netId, ownerId) => { + const entity = world.createEntity(`player_${netId}`); + + const identity = new NetworkIdentity(netId, ownerId); + entity.addComponent(identity); + entity.addComponent(new NetworkTransform()); + + // If local player, add input component + if (identity.bIsLocalPlayer) { + entity.addComponent(new LocalInputComponent()); + } + + 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); + } +} + +// Game loop +function gameLoop(deltaTime: number) { + world.update(deltaTime); +} + +startGame(); +``` + +### Handling Input + +```typescript +class LocalInputHandler extends EntitySystem { + private _inputSystem: NetworkInputSystem; + + constructor() { + super(Matcher.all(NetworkIdentity, LocalInputComponent)); + } + + protected onAddedToWorld(): void { + this._inputSystem = this.world.getSystem(NetworkInputSystem); + } + + protected processEntity(entity: Entity, dt: number): void { + const identity = entity.getComponent(NetworkIdentity); + if (!identity.bIsLocalPlayer) return; + + // Read keyboard input + let moveX = 0; + let moveY = 0; + + if (keyboard.isPressed('A')) moveX -= 1; + if (keyboard.isPressed('D')) moveX += 1; + if (keyboard.isPressed('W')) moveY += 1; + if (keyboard.isPressed('S')) moveY -= 1; + + if (moveX !== 0 || moveY !== 0) { + this._inputSystem.addMoveInput(moveX, moveY); + } + + if (keyboard.isJustPressed('Space')) { + this._inputSystem.addActionInput('jump'); + } + } +} +``` + +## Best Practices + +1. **Set appropriate sync rate**: Choose `tickRate` based on game type, action games typically need 20-60 Hz + +2. **Use interpolation delay**: Set appropriate `interpolationDelay` to balance latency and smoothness + +3. **Client prediction**: Use client-side prediction for local players to reduce input lag + +4. **Prefab management**: Register prefab factories for each networked entity type + +5. **Authority checks**: Use `bHasAuthority` to check entity control permissions + +6. **Connection state**: Monitor connection state changes, handle reconnection + +```typescript +networkService.setCallbacks({ + onConnected: () => console.log('Connected'), + onDisconnected: () => { + console.log('Disconnected'); + // Handle reconnection logic + } +}); +``` diff --git a/docs/modules/network/index.md b/docs/modules/network/index.md new file mode 100644 index 00000000..fab4d76b --- /dev/null +++ b/docs/modules/network/index.md @@ -0,0 +1,633 @@ +# 网络同步系统 (Network) + +`@esengine/network` 提供基于 TSRPC 的客户端-服务器网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。 + +## 概述 + +网络模块由三个包组成: + +| 包名 | 描述 | +|------|------| +| `@esengine/network` | 客户端 ECS 插件 | +| `@esengine/network-protocols` | 共享协议定义 | +| `@esengine/network-server` | 服务器端实现 | + +## 安装 + +```bash +# 客户端 +npm install @esengine/network + +# 服务器端 +npm install @esengine/network-server +``` + +## 快速开始 + +### 客户端 + +```typescript +import { World } 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); + +// 注册预制体工厂 +networkPlugin.registerPrefab('player', (netId, ownerId) => { + const entity = world.createEntity(`player_${netId}`); + entity.addComponent(new NetworkIdentity(netId, ownerId)); + entity.addComponent(new NetworkTransform()); + // 添加其他组件... + return entity; +}); + +// 连接服务器 +await networkPlugin.connect('PlayerName'); +console.log('Connected! Client ID:', networkPlugin.localPlayerId); + +// 断开连接 +networkPlugin.disconnect(); +``` + +### 服务器端 + +```typescript +import { GameServer } from '@esengine/network-server'; + +const server = new GameServer({ + port: 3000, + roomConfig: { + maxPlayers: 16, + tickRate: 20 + } +}); + +await server.start(); +``` + +## 核心概念 + +### 架构 + +``` +客户端 服务器 +┌────────────────┐ ┌────────────────┐ +│ NetworkPlugin │◄──── WS ────► │ GameServer │ +│ ├─ Service │ │ ├─ Room │ +│ ├─ SyncSystem │ │ └─ Players │ +│ ├─ SpawnSystem │ └────────────────┘ +│ └─ InputSystem │ +└────────────────┘ +``` + +### 组件 + +#### NetworkIdentity + +网络标识组件,每个网络同步的实体必须拥有: + +```typescript +class NetworkIdentity extends Component { + netId: number; // 网络唯一 ID + ownerId: number; // 所有者客户端 ID + bIsLocalPlayer: boolean; // 是否为本地玩家 + bHasAuthority: boolean; // 是否有权限控制 +} +``` + +#### NetworkTransform + +网络变换组件,用于位置和旋转同步: + +```typescript +class NetworkTransform extends Component { + position: { x: number; y: number }; + rotation: number; + velocity: { x: number; y: number }; +} +``` + +### 系统 + +#### NetworkSyncSystem + +处理服务器状态同步和插值: + +- 接收服务器状态快照 +- 将状态存入快照缓冲区 +- 对远程实体进行插值平滑 + +#### NetworkSpawnSystem + +处理实体的网络生成和销毁: + +- 监听 Spawn/Despawn 消息 +- 使用注册的预制体工厂创建实体 +- 管理网络实体的生命周期 + +#### NetworkInputSystem + +处理本地玩家输入的网络发送: + +- 收集本地玩家输入 +- 发送输入到服务器 +- 支持移动和动作输入 + +## API 参考 + +### NetworkPlugin + +```typescript +class NetworkPlugin { + constructor(config: INetworkPluginConfig); + + // 安装插件 + install(services: ServiceContainer): void; + + // 连接服务器 + connect(playerName: string, roomId?: string): Promise; + + // 断开连接 + disconnect(): void; + + // 注册预制体工厂 + registerPrefab(prefab: string, factory: PrefabFactory): void; + + // 属性 + readonly localPlayerId: number | null; + readonly isConnected: boolean; +} +``` + +**配置选项:** + +| 属性 | 类型 | 必需 | 描述 | +|------|------|------|------| +| `serverUrl` | `string` | 是 | WebSocket 服务器地址 | + +### NetworkService + +网络服务,管理 WebSocket 连接: + +```typescript +class NetworkService { + // 连接状态 + readonly state: ENetworkState; + readonly isConnected: boolean; + readonly clientId: number | null; + readonly roomId: string | null; + + // 连接控制 + connect(serverUrl: string): Promise; + disconnect(): void; + + // 加入房间 + join(playerName: string, roomId?: string): Promise; + + // 发送输入 + sendInput(input: IPlayerInput): void; + + // 事件回调 + setCallbacks(callbacks: Partial): void; +} +``` + +**网络状态枚举:** + +```typescript +enum ENetworkState { + Disconnected = 'disconnected', + Connecting = 'connecting', + Connected = 'connected', + Joining = 'joining', + Joined = 'joined' +} +``` + +**回调接口:** + +```typescript +interface INetworkCallbacks { + onConnected?: () => void; + onDisconnected?: () => void; + onJoined?: (clientId: number, roomId: string) => void; + onSync?: (msg: MsgSync) => void; + onSpawn?: (msg: MsgSpawn) => void; + onDespawn?: (msg: MsgDespawn) => void; +} +``` + +### 预制体工厂 + +```typescript +type PrefabFactory = (netId: number, ownerId: number) => Entity; +``` + +注册预制体工厂用于网络实体的创建: + +```typescript +networkPlugin.registerPrefab('enemy', (netId, ownerId) => { + const entity = world.createEntity(`enemy_${netId}`); + entity.addComponent(new NetworkIdentity(netId, ownerId)); + entity.addComponent(new NetworkTransform()); + entity.addComponent(new EnemyComponent()); + return entity; +}); +``` + +### 输入系统 + +#### NetworkInputSystem + +```typescript +class NetworkInputSystem extends EntitySystem { + // 添加移动输入 + addMoveInput(x: number, y: number): void; + + // 添加动作输入 + addActionInput(action: string): void; + + // 清除输入 + clearInput(): void; +} +``` + +使用示例: + +```typescript +const inputSystem = world.getSystem(NetworkInputSystem); + +// 处理键盘输入 +if (keyboard.isPressed('W')) { + inputSystem.addMoveInput(0, 1); +} +if (keyboard.isPressed('Space')) { + inputSystem.addActionInput('jump'); +} +``` + +## 状态同步 + +### 快照缓冲区 + +用于存储服务器状态快照并进行插值: + +```typescript +import { createSnapshotBuffer, type IStateSnapshot } from '@esengine/network'; + +const buffer = createSnapshotBuffer({ + maxSnapshots: 30, // 最大快照数 + interpolationDelay: 100 // 插值延迟 (ms) +}); + +// 添加快照 +buffer.addSnapshot({ + time: serverTime, + entities: states +}); + +// 获取插值状态 +const interpolated = buffer.getInterpolatedState(clientTime); +``` + +### 变换插值器 + +#### 线性插值器 + +```typescript +import { createTransformInterpolator } from '@esengine/network'; + +const interpolator = createTransformInterpolator(); + +// 添加状态 +interpolator.addState(time, { x: 0, y: 0, rotation: 0 }); + +// 获取插值结果 +const state = interpolator.getInterpolatedState(currentTime); +``` + +#### Hermite 插值器 + +使用 Hermite 样条实现更平滑的插值: + +```typescript +import { createHermiteTransformInterpolator } from '@esengine/network'; + +const interpolator = createHermiteTransformInterpolator({ + bufferSize: 10 +}); + +// 添加带速度的状态 +interpolator.addState(time, { + x: 100, + y: 200, + rotation: 0, + vx: 5, + vy: 0 +}); + +// 获取平滑的插值结果 +const state = interpolator.getInterpolatedState(currentTime); +``` + +### 客户端预测 + +实现客户端预测和服务器校正: + +```typescript +import { createClientPrediction } from '@esengine/network'; + +const prediction = createClientPrediction({ + maxPredictedInputs: 60, + reconciliationThreshold: 0.1 +}); + +// 预测输入 +const seq = prediction.predict(inputState, currentState, (state, input) => { + // 应用输入到状态 + return applyInput(state, input); +}); + +// 服务器校正 +const corrected = prediction.reconcile( + serverState, + serverSeq, + (state, input) => applyInput(state, input) +); +``` + +## 服务器端 + +### GameServer + +```typescript +import { GameServer } from '@esengine/network-server'; + +const server = new GameServer({ + port: 3000, + roomConfig: { + maxPlayers: 16, // 房间最大玩家数 + tickRate: 20 // 同步频率 (Hz) + } +}); + +// 启动服务器 +await server.start(); + +// 获取房间 +const room = server.getOrCreateRoom('room-id'); + +// 停止服务器 +await server.stop(); +``` + +### Room + +```typescript +class Room { + readonly id: string; + readonly playerCount: number; + readonly isFull: boolean; + + // 添加玩家 + addPlayer(name: string, connection: Connection): IPlayer | null; + + // 移除玩家 + removePlayer(clientId: number): void; + + // 获取玩家 + getPlayer(clientId: number): IPlayer | undefined; + + // 处理输入 + handleInput(clientId: number, input: IPlayerInput): void; + + // 销毁房间 + destroy(): void; +} +``` + +**玩家接口:** + +```typescript +interface IPlayer { + clientId: number; // 客户端 ID + name: string; // 玩家名称 + connection: Connection; // 连接对象 + netId: number; // 网络实体 ID +} +``` + +## 协议类型 + +### 消息类型 + +```typescript +// 状态同步消息 +interface MsgSync { + time: number; + entities: IEntityState[]; +} + +// 实体状态 +interface IEntityState { + netId: number; + pos?: Vec2; + rot?: number; +} + +// 生成消息 +interface MsgSpawn { + netId: number; + ownerId: number; + prefab: string; + pos: Vec2; + rot: number; +} + +// 销毁消息 +interface MsgDespawn { + netId: number; +} + +// 输入消息 +interface MsgInput { + input: IPlayerInput; +} + +// 玩家输入 +interface IPlayerInput { + seq?: number; + moveDir?: Vec2; + actions?: string[]; +} +``` + +### API 类型 + +```typescript +// 加入请求 +interface ReqJoin { + playerName: string; + roomId?: string; +} + +// 加入响应 +interface ResJoin { + clientId: number; + roomId: string; + playerCount: number; +} +``` + +## 蓝图节点 + +网络模块提供了可视化脚本支持的蓝图节点: + +- `IsLocalPlayer` - 检查实体是否为本地玩家 +- `IsServer` - 检查是否运行在服务器端 +- `HasAuthority` - 检查是否有权限控制实体 +- `GetNetworkId` - 获取实体的网络 ID +- `GetLocalPlayerId` - 获取本地玩家 ID + +## 服务令牌 + +用于依赖注入: + +```typescript +import { + NetworkServiceToken, + NetworkSyncSystemToken, + NetworkSpawnSystemToken, + NetworkInputSystemToken +} from '@esengine/network'; + +// 获取服务 +const networkService = services.get(NetworkServiceToken); +``` + +## 实际示例 + +### 完整的多人游戏客户端 + +```typescript +import { World, EntitySystem, Matcher } from '@esengine/ecs-framework'; +import { + NetworkPlugin, + NetworkIdentity, + NetworkTransform, + NetworkInputSystem +} from '@esengine/network'; + +// 创建游戏世界 +const world = new World(); + +// 配置网络插件 +const networkPlugin = new NetworkPlugin({ + serverUrl: 'ws://localhost:3000' +}); +networkPlugin.install(world.services); + +// 注册玩家预制体 +networkPlugin.registerPrefab('player', (netId, ownerId) => { + const entity = world.createEntity(`player_${netId}`); + + const identity = new NetworkIdentity(netId, ownerId); + entity.addComponent(identity); + entity.addComponent(new NetworkTransform()); + + // 如果是本地玩家,添加输入组件 + if (identity.bIsLocalPlayer) { + entity.addComponent(new LocalInputComponent()); + } + + return entity; +}); + +// 连接服务器 +async function startGame() { + try { + await networkPlugin.connect('Player1'); + console.log('已连接! 玩家 ID:', networkPlugin.localPlayerId); + } catch (error) { + console.error('连接失败:', error); + } +} + +// 游戏循环 +function gameLoop(deltaTime: number) { + world.update(deltaTime); +} + +startGame(); +``` + +### 处理输入 + +```typescript +class LocalInputHandler extends EntitySystem { + private _inputSystem: NetworkInputSystem; + + constructor() { + super(Matcher.all(NetworkIdentity, LocalInputComponent)); + } + + protected onAddedToWorld(): void { + this._inputSystem = this.world.getSystem(NetworkInputSystem); + } + + protected processEntity(entity: Entity, dt: number): void { + const identity = entity.getComponent(NetworkIdentity); + if (!identity.bIsLocalPlayer) return; + + // 读取键盘输入 + let moveX = 0; + let moveY = 0; + + if (keyboard.isPressed('A')) moveX -= 1; + if (keyboard.isPressed('D')) moveX += 1; + if (keyboard.isPressed('W')) moveY += 1; + if (keyboard.isPressed('S')) moveY -= 1; + + if (moveX !== 0 || moveY !== 0) { + this._inputSystem.addMoveInput(moveX, moveY); + } + + if (keyboard.isJustPressed('Space')) { + this._inputSystem.addActionInput('jump'); + } + } +} +``` + +## 最佳实践 + +1. **合理设置同步频率**:根据游戏类型选择合适的 `tickRate`,动作游戏通常需要 20-60 Hz + +2. **使用插值延迟**:设置适当的 `interpolationDelay` 来平衡延迟和平滑度 + +3. **客户端预测**:对于本地玩家使用客户端预测减少输入延迟 + +4. **预制体管理**:为每种网络实体类型注册对应的预制体工厂 + +5. **权限检查**:使用 `bHasAuthority` 检查是否有权限修改实体 + +6. **连接状态**:监听连接状态变化,处理断线重连 + +```typescript +networkService.setCallbacks({ + onConnected: () => console.log('已连接'), + onDisconnected: () => { + console.log('已断开'); + // 处理重连逻辑 + } +}); +``` diff --git a/packages/tools/cli/src/cli.ts b/packages/tools/cli/src/cli.ts index 58739066..008624e5 100644 --- a/packages/tools/cli/src/cli.ts +++ b/packages/tools/cli/src/cli.ts @@ -26,52 +26,68 @@ function printLogo(): void { console.log(); } +// ============================================================================= +// 项目检测 | Project Detection +// ============================================================================= + /** - * @zh 检测是否存在 *.laya 文件 - * @en Check if *.laya file exists + * @zh 检查文件或目录是否存在 + * @en Check if file or directory exists */ -function hasLayaProjectFile(cwd: string): boolean { +const exists = (cwd: string, ...paths: string[]): boolean => + paths.some(p => fs.existsSync(path.join(cwd, p))); + +/** + * @zh 安全读取 JSON 文件 + * @en Safely read JSON file + */ +function readJson>(filePath: string): T | null { try { - const files = fs.readdirSync(cwd); - return files.some(f => f.endsWith('.laya')); + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } catch { + return null; + } +} + +/** + * @zh 检查目录中是否有匹配后缀的文件 + * @en Check if directory contains files with matching extension + */ +function hasFileWithExt(cwd: string, ext: string): boolean { + try { + return fs.readdirSync(cwd).some(f => f.endsWith(ext)); } catch { return false; } } /** - * @zh 检测 Cocos Creator 版本 - * @en Detect Cocos Creator version + * @zh 从 package.json 获取 Cocos Creator 版本 + * @en Get Cocos Creator version from package.json */ -function detectCocosVersion(cwd: string): 'cocos' | 'cocos2' | null { - // Cocos 3.x: 检查 cc.config.json 或 extensions 目录 - if (fs.existsSync(path.join(cwd, 'cc.config.json')) || - fs.existsSync(path.join(cwd, 'extensions'))) { - return 'cocos'; - } +function getCocosVersionFromPackage(cwd: string): string | null { + const pkg = readJson<{ creator?: { version?: string } }>(path.join(cwd, 'package.json')); + return pkg?.creator?.version ?? null; +} - // 检查 project.json 中的版本号 - const projectJsonPath = path.join(cwd, 'project.json'); - if (fs.existsSync(projectJsonPath)) { - try { - const project = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8')); - // Cocos 2.x project.json 有 engine-version 字段 - if (project['engine-version'] || project.engine) { - const version = project['engine-version'] || project.engine || ''; - // 2.x 版本格式: "cocos-creator-js-2.4.x" 或 "2.4.x" - if (version.includes('2.') || version.startsWith('2')) { - return 'cocos2'; - } - } - // 有 project.json 但没有版本信息,假设是 3.x - return 'cocos'; - } catch { - // 解析失败,假设是 3.x - return 'cocos'; - } - } +/** + * @zh 从 project.json 获取 Cocos 2.x 版本 + * @en Get Cocos 2.x version from project.json + */ +function getCocos2VersionFromProject(cwd: string): string | null { + const project = readJson<{ 'engine-version'?: string; engine?: string }>( + path.join(cwd, 'project.json') + ); + return project?.['engine-version'] ?? project?.engine ?? null; +} - return null; +/** + * @zh 判断版本号属于哪个大版本 + * @en Determine major version from version string + */ +function getMajorVersion(version: string): number | null { + const match = version.match(/^(\d+)\./); + return match ? parseInt(match[1], 10) : null; } /** @@ -79,23 +95,35 @@ function detectCocosVersion(cwd: string): 'cocos' | 'cocos2' | null { * @en Detect project type */ function detectProjectType(cwd: string): PlatformType | null { - // Laya: 检查 *.laya 文件 或 .laya 目录 或 laya.json - if (hasLayaProjectFile(cwd) || - fs.existsSync(path.join(cwd, '.laya')) || - fs.existsSync(path.join(cwd, 'laya.json'))) { + // Laya: *.laya 文件、.laya 目录、laya.json + if (hasFileWithExt(cwd, '.laya') || exists(cwd, '.laya', 'laya.json')) { return 'laya'; } - // Cocos Creator: 检查 assets 目录 - if (fs.existsSync(path.join(cwd, 'assets'))) { - const cocosVersion = detectCocosVersion(cwd); - if (cocosVersion) { - return cocosVersion; - } + // Cocos Creator: 检查 package.json 中的 creator.version + const cocosVersion = getCocosVersionFromPackage(cwd); + if (cocosVersion) { + const major = getMajorVersion(cocosVersion); + if (major === 2) return 'cocos2'; + if (major && major >= 3) return 'cocos'; } - // Node.js: 检查 package.json - if (fs.existsSync(path.join(cwd, 'package.json'))) { + // Cocos 3.x: .creator 目录、settings 目录、cc.config.json、extensions 目录 + if (exists(cwd, '.creator', 'settings', 'cc.config.json', 'extensions')) { + return 'cocos'; + } + + // Cocos 2.x: project.json 中的 engine-version + const cocos2Version = getCocos2VersionFromProject(cwd); + if (cocos2Version) { + if (cocos2Version.includes('2.') || cocos2Version.startsWith('2')) { + return 'cocos2'; + } + return 'cocos'; + } + + // Node.js: 有 package.json 但不是 Cocos + if (exists(cwd, 'package.json')) { return 'nodejs'; } diff --git a/packages/tools/cli/src/modules.ts b/packages/tools/cli/src/modules.ts index 53af2cdd..04eaa8a1 100644 --- a/packages/tools/cli/src/modules.ts +++ b/packages/tools/cli/src/modules.ts @@ -94,6 +94,34 @@ export const AVAILABLE_MODULES: ModuleInfo[] = [ version: 'latest', description: '可视化脚本系统 | Visual scripting system', category: 'utility' + }, + + // Network + { + id: 'network', + name: 'Network', + package: '@esengine/network', + version: 'latest', + description: '网络同步客户端 | Network sync client', + category: 'network', + dependencies: ['network-protocols'] + }, + { + id: 'network-protocols', + name: 'Network Protocols', + package: '@esengine/network-protocols', + version: 'latest', + description: '网络共享协议 | Shared network protocols', + category: 'network' + }, + { + id: 'network-server', + name: 'Network Server', + package: '@esengine/network-server', + version: 'latest', + description: '网络游戏服务器 | Network game server', + category: 'network', + dependencies: ['network-protocols'] } ];