Compare commits
9 Commits
@esengine/
...
@esengine/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a88c6f2fc | ||
|
|
b0b95c60b4 | ||
|
|
683ac7a7d4 | ||
|
|
1e240e86f2 | ||
|
|
4d6c2fe7ff | ||
|
|
67c06720c5 | ||
|
|
33e98b9a75 | ||
|
|
a42f2412d7 | ||
|
|
fdb19a33fb |
@@ -33,7 +33,6 @@
|
||||
"@esengine/physics-rapier2d",
|
||||
"@esengine/rapier2d",
|
||||
"@esengine/world-streaming",
|
||||
"@esengine/network-server",
|
||||
"@esengine/editor-core",
|
||||
"@esengine/editor-runtime",
|
||||
"@esengine/editor-app",
|
||||
|
||||
727
docs/en/modules/network/index.md
Normal file
727
docs/en/modules/network/index.md
Normal file
@@ -0,0 +1,727 @@
|
||||
# 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 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 { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
NetworkPlugin,
|
||||
NetworkIdentity,
|
||||
NetworkTransform
|
||||
} from '@esengine/network';
|
||||
|
||||
// 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', (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;
|
||||
});
|
||||
|
||||
// Connect to server
|
||||
const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
|
||||
if (success) {
|
||||
console.log('Connected!');
|
||||
}
|
||||
|
||||
// Game loop
|
||||
function gameLoop(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
|
||||
// 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';
|
||||
|
||||
const server = new GameServer({
|
||||
port: 3000,
|
||||
roomConfig: {
|
||||
maxPlayers: 16,
|
||||
tickRate: 20
|
||||
}
|
||||
});
|
||||
|
||||
await server.start();
|
||||
console.log('Server started on ws://localhost:3000');
|
||||
```
|
||||
|
||||
## 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<void>;
|
||||
|
||||
// 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<void>;
|
||||
disconnect(): void;
|
||||
|
||||
// Join room
|
||||
join(playerName: string, roomId?: string): Promise<ResJoin>;
|
||||
|
||||
// Send input
|
||||
sendInput(input: IPlayerInput): void;
|
||||
|
||||
// Event callbacks
|
||||
setCallbacks(callbacks: Partial<INetworkCallbacks>): 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 = (scene: Scene, spawn: MsgSpawn) => Entity;
|
||||
```
|
||||
|
||||
Register prefab factories for network entity creation:
|
||||
|
||||
```typescript
|
||||
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;
|
||||
});
|
||||
```
|
||||
|
||||
### 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
|
||||
// Send input via NetworkPlugin (recommended)
|
||||
networkPlugin.sendMoveInput(0, 1); // Movement
|
||||
networkPlugin.sendActionInput('jump'); // Action
|
||||
|
||||
// Or use inputSystem directly
|
||||
const inputSystem = networkPlugin.inputSystem;
|
||||
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<IStateSnapshot>({
|
||||
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 { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
NetworkPlugin,
|
||||
NetworkIdentity,
|
||||
NetworkTransform
|
||||
} from '@esengine/network';
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
async function initGame() {
|
||||
Core.create({ debug: false });
|
||||
|
||||
const scene = new GameScene();
|
||||
Core.setScene(scene);
|
||||
|
||||
// Install network plugin
|
||||
const networkPlugin = new NetworkPlugin();
|
||||
await Core.installPlugin(networkPlugin);
|
||||
|
||||
// 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 networkPlugin;
|
||||
}
|
||||
|
||||
// Game loop
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
initGame();
|
||||
```
|
||||
|
||||
### Handling Input
|
||||
|
||||
```typescript
|
||||
class LocalInputHandler extends EntitySystem {
|
||||
private _networkPlugin: NetworkPlugin | null = null;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(NetworkIdentity, LocalInputComponent));
|
||||
}
|
||||
|
||||
protected onAddedToScene(): void {
|
||||
// Get NetworkPlugin reference
|
||||
this._networkPlugin = Core.getPlugin(NetworkPlugin);
|
||||
}
|
||||
|
||||
protected processEntity(entity: Entity, dt: number): void {
|
||||
if (!this._networkPlugin) return;
|
||||
|
||||
const identity = entity.getComponent(NetworkIdentity)!;
|
||||
if (!identity.isLocalPlayer) 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._networkPlugin.sendMoveInput(moveX, moveY);
|
||||
}
|
||||
|
||||
if (keyboard.isJustPressed('Space')) {
|
||||
this._networkPlugin.sendActionInput('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
|
||||
}
|
||||
});
|
||||
```
|
||||
727
docs/modules/network/index.md
Normal file
727
docs/modules/network/index.md
Normal file
@@ -0,0 +1,727 @@
|
||||
# 网络同步系统 (Network)
|
||||
|
||||
`@esengine/network` 提供基于 TSRPC 的客户端-服务器网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。
|
||||
|
||||
## 概述
|
||||
|
||||
网络模块由三个包组成:
|
||||
|
||||
| 包名 | 描述 |
|
||||
|------|------|
|
||||
| `@esengine/network` | 客户端 ECS 插件 |
|
||||
| `@esengine/network-protocols` | 共享协议定义 |
|
||||
| `@esengine/network-server` | 服务器端实现 |
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 客户端
|
||||
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 { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
NetworkPlugin,
|
||||
NetworkIdentity,
|
||||
NetworkTransform
|
||||
} from '@esengine/network';
|
||||
|
||||
// 定义游戏场景
|
||||
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', (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;
|
||||
});
|
||||
|
||||
// 连接服务器
|
||||
const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
|
||||
if (success) {
|
||||
console.log('Connected!');
|
||||
}
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(dt: number) {
|
||||
Core.update(dt);
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
await networkPlugin.disconnect();
|
||||
```
|
||||
|
||||
### 服务器端
|
||||
|
||||
使用 CLI 创建服务端项目后,默认生成的代码已经配置好了 GameServer:
|
||||
|
||||
```typescript
|
||||
import { GameServer } from '@esengine/network-server';
|
||||
|
||||
const server = new GameServer({
|
||||
port: 3000,
|
||||
roomConfig: {
|
||||
maxPlayers: 16,
|
||||
tickRate: 20
|
||||
}
|
||||
});
|
||||
|
||||
await server.start();
|
||||
console.log('Server started on ws://localhost:3000');
|
||||
```
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 架构
|
||||
|
||||
```
|
||||
客户端 服务器
|
||||
┌────────────────┐ ┌────────────────┐
|
||||
│ 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<void>;
|
||||
|
||||
// 断开连接
|
||||
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<void>;
|
||||
disconnect(): void;
|
||||
|
||||
// 加入房间
|
||||
join(playerName: string, roomId?: string): Promise<ResJoin>;
|
||||
|
||||
// 发送输入
|
||||
sendInput(input: IPlayerInput): void;
|
||||
|
||||
// 事件回调
|
||||
setCallbacks(callbacks: Partial<INetworkCallbacks>): 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 = (scene: Scene, spawn: MsgSpawn) => Entity;
|
||||
```
|
||||
|
||||
注册预制体工厂用于网络实体的创建:
|
||||
|
||||
```typescript
|
||||
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;
|
||||
});
|
||||
```
|
||||
|
||||
### 输入系统
|
||||
|
||||
#### NetworkInputSystem
|
||||
|
||||
```typescript
|
||||
class NetworkInputSystem extends EntitySystem {
|
||||
// 添加移动输入
|
||||
addMoveInput(x: number, y: number): void;
|
||||
|
||||
// 添加动作输入
|
||||
addActionInput(action: string): void;
|
||||
|
||||
// 清除输入
|
||||
clearInput(): void;
|
||||
}
|
||||
```
|
||||
|
||||
使用示例:
|
||||
|
||||
```typescript
|
||||
// 通过 NetworkPlugin 发送输入(推荐)
|
||||
networkPlugin.sendMoveInput(0, 1); // 移动
|
||||
networkPlugin.sendActionInput('jump'); // 动作
|
||||
|
||||
// 或直接使用 inputSystem
|
||||
const inputSystem = networkPlugin.inputSystem;
|
||||
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<IStateSnapshot>({
|
||||
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 { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
import {
|
||||
NetworkPlugin,
|
||||
NetworkIdentity,
|
||||
NetworkTransform
|
||||
} from '@esengine/network';
|
||||
|
||||
// 定义游戏场景
|
||||
class GameScene extends Scene {
|
||||
initialize(): void {
|
||||
this.name = 'MultiplayerGame';
|
||||
// 网络系统由 NetworkPlugin 自动添加
|
||||
// 添加自定义系统
|
||||
this.addSystem(new LocalInputHandler());
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
async function initGame() {
|
||||
Core.create({ debug: false });
|
||||
|
||||
const scene = new GameScene();
|
||||
Core.setScene(scene);
|
||||
|
||||
// 安装网络插件
|
||||
const networkPlugin = new NetworkPlugin();
|
||||
await Core.installPlugin(networkPlugin);
|
||||
|
||||
// 注册玩家预制体
|
||||
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 networkPlugin;
|
||||
}
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
|
||||
initGame();
|
||||
```
|
||||
|
||||
### 处理输入
|
||||
|
||||
```typescript
|
||||
class LocalInputHandler extends EntitySystem {
|
||||
private _networkPlugin: NetworkPlugin | null = null;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(NetworkIdentity, LocalInputComponent));
|
||||
}
|
||||
|
||||
protected onAddedToScene(): void {
|
||||
// 获取 NetworkPlugin 引用
|
||||
this._networkPlugin = Core.getPlugin(NetworkPlugin);
|
||||
}
|
||||
|
||||
protected processEntity(entity: Entity, dt: number): void {
|
||||
if (!this._networkPlugin) return;
|
||||
|
||||
const identity = entity.getComponent(NetworkIdentity)!;
|
||||
if (!identity.isLocalPlayer) 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._networkPlugin.sendMoveInput(moveX, moveY);
|
||||
}
|
||||
|
||||
if (keyboard.isJustPressed('Space')) {
|
||||
this._networkPlugin.sendActionInput('jump');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **合理设置同步频率**:根据游戏类型选择合适的 `tickRate`,动作游戏通常需要 20-60 Hz
|
||||
|
||||
2. **使用插值延迟**:设置适当的 `interpolationDelay` 来平衡延迟和平滑度
|
||||
|
||||
3. **客户端预测**:对于本地玩家使用客户端预测减少输入延迟
|
||||
|
||||
4. **预制体管理**:为每种网络实体类型注册对应的预制体工厂
|
||||
|
||||
5. **权限检查**:使用 `bHasAuthority` 检查是否有权限修改实体
|
||||
|
||||
6. **连接状态**:监听连接状态变化,处理断线重连
|
||||
|
||||
```typescript
|
||||
networkService.setCallbacks({
|
||||
onConnected: () => console.log('已连接'),
|
||||
onDisconnected: () => {
|
||||
console.log('已断开');
|
||||
// 处理重连逻辑
|
||||
}
|
||||
});
|
||||
```
|
||||
17
packages/network-ext/network-server/CHANGELOG.md
Normal file
17
packages/network-ext/network-server/CHANGELOG.md
Normal 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 兼容性问题
|
||||
@@ -1,15 +1,21 @@
|
||||
{
|
||||
"name": "@esengine/network-server",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#352](https://github.com/esengine/esengine/pull/352) [`33e98b9`](https://github.com/esengine/esengine/commit/33e98b9a750f9fe684c36f1937c1afa38da36315) Thanks [@esengine](https://github.com/esengine)! - fix(cli): 修复 Cocos Creator 3.x 项目检测逻辑
|
||||
- 优先检查 package.json 中的 creator.version 字段
|
||||
- 添加 .creator 和 settings 目录检测
|
||||
- 重构检测代码,提取通用辅助函数
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/cli",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"description": "CLI tool for adding ESEngine ECS framework to existing projects",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -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<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 {
|
||||
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<typeof setInterval> | 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/)
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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<T = Record<string, unknown>>(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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user