Compare commits
2 Commits
@esengine/
...
@esengine/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67c06720c5 | ||
|
|
33e98b9a75 |
633
docs/en/modules/network/index.md
Normal file
633
docs/en/modules/network/index.md
Normal file
@@ -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<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 = (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<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 { 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
633
docs/modules/network/index.md
Normal file
633
docs/modules/network/index.md
Normal file
@@ -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<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 = (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<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 { 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('已断开');
|
||||||
|
// 处理重连逻辑
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
# @esengine/cli
|
# @esengine/cli
|
||||||
|
|
||||||
|
## 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
|
## 1.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/cli",
|
"name": "@esengine/cli",
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"description": "CLI tool for adding ESEngine ECS framework to existing projects",
|
"description": "CLI tool for adding ESEngine ECS framework to existing projects",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -26,52 +26,68 @@ function printLogo(): void {
|
|||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 项目检测 | Project Detection
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 检测是否存在 *.laya 文件
|
* @zh 检查文件或目录是否存在
|
||||||
* @en Check if *.laya file exists
|
* @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 {
|
try {
|
||||||
const files = fs.readdirSync(cwd);
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||||
return files.some(f => f.endsWith('.laya'));
|
} 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 {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 检测 Cocos Creator 版本
|
* @zh 从 package.json 获取 Cocos Creator 版本
|
||||||
* @en Detect Cocos Creator version
|
* @en Get Cocos Creator version from package.json
|
||||||
*/
|
*/
|
||||||
function detectCocosVersion(cwd: string): 'cocos' | 'cocos2' | null {
|
function getCocosVersionFromPackage(cwd: string): string | null {
|
||||||
// Cocos 3.x: 检查 cc.config.json 或 extensions 目录
|
const pkg = readJson<{ creator?: { version?: string } }>(path.join(cwd, 'package.json'));
|
||||||
if (fs.existsSync(path.join(cwd, 'cc.config.json')) ||
|
return pkg?.creator?.version ?? null;
|
||||||
fs.existsSync(path.join(cwd, 'extensions'))) {
|
}
|
||||||
return 'cocos';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 project.json 中的版本号
|
/**
|
||||||
const projectJsonPath = path.join(cwd, 'project.json');
|
* @zh 从 project.json 获取 Cocos 2.x 版本
|
||||||
if (fs.existsSync(projectJsonPath)) {
|
* @en Get Cocos 2.x version from project.json
|
||||||
try {
|
*/
|
||||||
const project = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
|
function getCocos2VersionFromProject(cwd: string): string | null {
|
||||||
// Cocos 2.x project.json 有 engine-version 字段
|
const project = readJson<{ 'engine-version'?: string; engine?: string }>(
|
||||||
if (project['engine-version'] || project.engine) {
|
path.join(cwd, 'project.json')
|
||||||
const version = project['engine-version'] || project.engine || '';
|
);
|
||||||
// 2.x 版本格式: "cocos-creator-js-2.4.x" 或 "2.4.x"
|
return project?.['engine-version'] ?? project?.engine ?? null;
|
||||||
if (version.includes('2.') || version.startsWith('2')) {
|
}
|
||||||
return 'cocos2';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 有 project.json 但没有版本信息,假设是 3.x
|
|
||||||
return 'cocos';
|
|
||||||
} catch {
|
|
||||||
// 解析失败,假设是 3.x
|
|
||||||
return 'cocos';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
* @en Detect project type
|
||||||
*/
|
*/
|
||||||
function detectProjectType(cwd: string): PlatformType | null {
|
function detectProjectType(cwd: string): PlatformType | null {
|
||||||
// Laya: 检查 *.laya 文件 或 .laya 目录 或 laya.json
|
// Laya: *.laya 文件、.laya 目录、laya.json
|
||||||
if (hasLayaProjectFile(cwd) ||
|
if (hasFileWithExt(cwd, '.laya') || exists(cwd, '.laya', 'laya.json')) {
|
||||||
fs.existsSync(path.join(cwd, '.laya')) ||
|
|
||||||
fs.existsSync(path.join(cwd, 'laya.json'))) {
|
|
||||||
return 'laya';
|
return 'laya';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cocos Creator: 检查 assets 目录
|
// Cocos Creator: 检查 package.json 中的 creator.version
|
||||||
if (fs.existsSync(path.join(cwd, 'assets'))) {
|
const cocosVersion = getCocosVersionFromPackage(cwd);
|
||||||
const cocosVersion = detectCocosVersion(cwd);
|
if (cocosVersion) {
|
||||||
if (cocosVersion) {
|
const major = getMajorVersion(cocosVersion);
|
||||||
return cocosVersion;
|
if (major === 2) return 'cocos2';
|
||||||
}
|
if (major && major >= 3) return 'cocos';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node.js: 检查 package.json
|
// Cocos 3.x: .creator 目录、settings 目录、cc.config.json、extensions 目录
|
||||||
if (fs.existsSync(path.join(cwd, 'package.json'))) {
|
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';
|
return 'nodejs';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,34 @@ export const AVAILABLE_MODULES: ModuleInfo[] = [
|
|||||||
version: 'latest',
|
version: 'latest',
|
||||||
description: '可视化脚本系统 | Visual scripting system',
|
description: '可视化脚本系统 | Visual scripting system',
|
||||||
category: 'utility'
|
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']
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user