Compare commits
14 Commits
@esengine/
...
@esengine/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7f8208b6f | ||
|
|
5131ec3c52 | ||
|
|
7d74623710 | ||
|
|
044463dd5f | ||
|
|
ce2db4e48a | ||
|
|
0a88c6f2fc | ||
|
|
b0b95c60b4 | ||
|
|
683ac7a7d4 | ||
|
|
1e240e86f2 | ||
|
|
4d6c2fe7ff | ||
|
|
67c06720c5 | ||
|
|
33e98b9a75 | ||
|
|
a42f2412d7 | ||
|
|
fdb19a33fb |
@@ -33,7 +33,6 @@
|
|||||||
"@esengine/physics-rapier2d",
|
"@esengine/physics-rapier2d",
|
||||||
"@esengine/rapier2d",
|
"@esengine/rapier2d",
|
||||||
"@esengine/world-streaming",
|
"@esengine/world-streaming",
|
||||||
"@esengine/network-server",
|
|
||||||
"@esengine/editor-core",
|
"@esengine/editor-core",
|
||||||
"@esengine/editor-runtime",
|
"@esengine/editor-runtime",
|
||||||
"@esengine/editor-app",
|
"@esengine/editor-app",
|
||||||
|
|||||||
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('已断开');
|
||||||
|
// 处理重连逻辑
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
15
packages/framework/behavior-tree/CHANGELOG.md
Normal file
15
packages/framework/behavior-tree/CHANGELOG.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# @esengine/behavior-tree
|
||||||
|
|
||||||
|
## 1.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
|
||||||
|
- @esengine/ecs-framework@2.4.4
|
||||||
|
|
||||||
|
## 1.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
|
||||||
|
- @esengine/ecs-framework@2.4.3
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/behavior-tree",
|
"name": "@esengine/behavior-tree",
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"description": "ECS-based AI behavior tree system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
"description": "ECS-based AI behavior tree system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
|
|||||||
15
packages/framework/blueprint/CHANGELOG.md
Normal file
15
packages/framework/blueprint/CHANGELOG.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# @esengine/blueprint
|
||||||
|
|
||||||
|
## 1.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
|
||||||
|
- @esengine/ecs-framework@2.4.4
|
||||||
|
|
||||||
|
## 1.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
|
||||||
|
- @esengine/ecs-framework@2.4.3
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/blueprint",
|
"name": "@esengine/blueprint",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
|
|||||||
16
packages/framework/core/CHANGELOG.md
Normal file
16
packages/framework/core/CHANGELOG.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# @esengine/ecs-framework
|
||||||
|
|
||||||
|
## 2.4.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8) Thanks [@esengine](https://github.com/esengine)! - fix(core): 修复 npm 发布目录配置,确保从 dist 目录发布以保持与 Cocos Creator 的兼容性
|
||||||
|
|
||||||
|
## 2.4.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#356](https://github.com/esengine/esengine/pull/356) [`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea) Thanks [@esengine](https://github.com/esengine)! - fix(core): 修复 World cleanup 在打包环境下的兼容性问题
|
||||||
|
- 使用 forEach 替代 spread + for...of 解构模式,避免某些打包工具(如 Cocos Creator)转换后的兼容性问题
|
||||||
|
- 重构 World 和 WorldManager 类,提升代码质量
|
||||||
|
- 提取默认配置为常量,统一双语注释格式
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/ecs-framework",
|
"name": "@esengine/ecs-framework",
|
||||||
"version": "2.4.2",
|
"version": "2.4.4",
|
||||||
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
||||||
"main": "dist/index.cjs",
|
"main": "dist/index.cjs",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
@@ -70,7 +70,8 @@
|
|||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://registry.npmjs.org/"
|
"registry": "https://registry.npmjs.org/",
|
||||||
|
"directory": "dist"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const logger = createLogger('World');
|
|||||||
* @zh 全局系统是在World级别运行的系统,不依赖特定Scene
|
* @zh 全局系统是在World级别运行的系统,不依赖特定Scene
|
||||||
* @en Global systems run at World level and don't depend on specific Scene
|
* @en Global systems run at World level and don't depend on specific Scene
|
||||||
*/
|
*/
|
||||||
export type IGlobalSystem = {
|
export interface IGlobalSystem {
|
||||||
/**
|
/**
|
||||||
* @zh 系统名称
|
* @zh 系统名称
|
||||||
* @en System name
|
* @en System name
|
||||||
@@ -49,7 +49,7 @@ export type IGlobalSystem = {
|
|||||||
* @zh World配置接口
|
* @zh World配置接口
|
||||||
* @en World configuration interface
|
* @en World configuration interface
|
||||||
*/
|
*/
|
||||||
export type IWorldConfig = {
|
export interface IWorldConfig {
|
||||||
/**
|
/**
|
||||||
* @zh World名称
|
* @zh World名称
|
||||||
* @en World name
|
* @en World name
|
||||||
@@ -77,12 +77,23 @@ export type IWorldConfig = {
|
|||||||
/**
|
/**
|
||||||
* @zh 自动清理阈值(毫秒),空Scene超过此时间后将被自动清理
|
* @zh 自动清理阈值(毫秒),空Scene超过此时间后将被自动清理
|
||||||
* @en Auto cleanup threshold (ms), empty scenes exceeding this time will be auto-cleaned
|
* @en Auto cleanup threshold (ms), empty scenes exceeding this time will be auto-cleaned
|
||||||
*
|
|
||||||
* @default 300000 (5 minutes)
|
* @default 300000 (5 minutes)
|
||||||
*/
|
*/
|
||||||
cleanupThresholdMs?: number;
|
cleanupThresholdMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh World默认配置
|
||||||
|
* @en World default configuration
|
||||||
|
*/
|
||||||
|
const DEFAULT_CONFIG: Required<IWorldConfig> = {
|
||||||
|
name: 'World',
|
||||||
|
debug: false,
|
||||||
|
maxScenes: 10,
|
||||||
|
autoCleanup: true,
|
||||||
|
cleanupThresholdMs: 5 * 60 * 1000
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh World类 - ECS世界管理器
|
* @zh World类 - ECS世界管理器
|
||||||
* @en World class - ECS world manager
|
* @en World class - ECS world manager
|
||||||
@@ -101,67 +112,66 @@ export type IWorldConfig = {
|
|||||||
* - World.services: World-level services (independent per World)
|
* - World.services: World-level services (independent per World)
|
||||||
* - Scene.services: Scene-level services (independent per Scene)
|
* - Scene.services: Scene-level services (independent per Scene)
|
||||||
*
|
*
|
||||||
* @zh 这种设计允许创建独立的游戏世界,如:
|
|
||||||
* - 游戏房间(每个房间一个World)
|
|
||||||
* - 不同的游戏模式
|
|
||||||
* - 独立的模拟环境
|
|
||||||
* @en This design allows creating independent game worlds like:
|
|
||||||
* - Game rooms (one World per room)
|
|
||||||
* - Different game modes
|
|
||||||
* - Independent simulation environments
|
|
||||||
*
|
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // @zh 创建游戏房间的World | @en Create World for game room
|
|
||||||
* const roomWorld = new World({ name: 'Room_001' });
|
* const roomWorld = new World({ name: 'Room_001' });
|
||||||
*
|
|
||||||
* // @zh 注册World级别的服务 | @en Register World-level service
|
|
||||||
* roomWorld.services.registerSingleton(RoomManager);
|
* roomWorld.services.registerSingleton(RoomManager);
|
||||||
*
|
*
|
||||||
* // @zh 在World中创建Scene | @en Create Scene in World
|
* const gameScene = roomWorld.createScene('game');
|
||||||
* const gameScene = roomWorld.createScene('game', new Scene());
|
* roomWorld.setSceneActive('game', true);
|
||||||
* const uiScene = roomWorld.createScene('ui', new Scene());
|
* roomWorld.start();
|
||||||
*
|
|
||||||
* // @zh 在Scene中使用World级别的服务 | @en Use World-level service in Scene
|
|
||||||
* const roomManager = roomWorld.services.resolve(RoomManager);
|
|
||||||
*
|
|
||||||
* // @zh 更新整个World | @en Update entire World
|
|
||||||
* roomWorld.update(deltaTime);
|
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class World {
|
export class World {
|
||||||
public readonly name: string;
|
public readonly name: string;
|
||||||
private readonly _config: IWorldConfig;
|
|
||||||
private readonly _scenes: Map<string, IScene> = new Map();
|
private readonly _config: Required<IWorldConfig>;
|
||||||
private readonly _activeScenes: Set<string> = new Set();
|
private readonly _scenes = new Map<string, IScene>();
|
||||||
|
private readonly _activeScenes = new Set<string>();
|
||||||
private readonly _globalSystems: IGlobalSystem[] = [];
|
private readonly _globalSystems: IGlobalSystem[] = [];
|
||||||
private readonly _services: ServiceContainer;
|
private readonly _services: ServiceContainer;
|
||||||
private _isActive: boolean = false;
|
private readonly _createdAt: number;
|
||||||
private _createdAt: number;
|
private _isActive = false;
|
||||||
|
|
||||||
constructor(config: IWorldConfig = {}) {
|
constructor(config: IWorldConfig = {}) {
|
||||||
this._config = {
|
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||||
name: 'World',
|
this.name = this._config.name;
|
||||||
debug: false,
|
|
||||||
maxScenes: 10,
|
|
||||||
autoCleanup: true,
|
|
||||||
cleanupThresholdMs: 5 * 60 * 1000,
|
|
||||||
...config
|
|
||||||
};
|
|
||||||
|
|
||||||
this.name = this._config.name!;
|
|
||||||
this._createdAt = Date.now();
|
this._createdAt = Date.now();
|
||||||
this._services = new ServiceContainer();
|
this._services = new ServiceContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh World级别的服务容器,用于管理World范围内的全局服务
|
* @zh World级别的服务容器
|
||||||
* @en World-level service container for managing World-scoped global services
|
* @en World-level service container
|
||||||
*/
|
*/
|
||||||
public get services(): ServiceContainer {
|
public get services(): ServiceContainer {
|
||||||
return this._services;
|
return this._services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 检查World是否激活
|
||||||
|
* @en Check if World is active
|
||||||
|
*/
|
||||||
|
public get isActive(): boolean {
|
||||||
|
return this._isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取Scene数量
|
||||||
|
* @en Get scene count
|
||||||
|
*/
|
||||||
|
public get sceneCount(): number {
|
||||||
|
return this._scenes.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取创建时间
|
||||||
|
* @en Get creation time
|
||||||
|
*/
|
||||||
|
public get createdAt(): number {
|
||||||
|
return this._createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 创建并添加Scene到World
|
* @zh 创建并添加Scene到World
|
||||||
* @en Create and add Scene to World
|
* @en Create and add Scene to World
|
||||||
@@ -169,32 +179,21 @@ export class World {
|
|||||||
* @param sceneName - @zh Scene名称 @en Scene name
|
* @param sceneName - @zh Scene名称 @en Scene name
|
||||||
* @param sceneInstance - @zh Scene实例(可选)@en Scene instance (optional)
|
* @param sceneInstance - @zh Scene实例(可选)@en Scene instance (optional)
|
||||||
* @returns @zh 创建的Scene实例 @en Created Scene instance
|
* @returns @zh 创建的Scene实例 @en Created Scene instance
|
||||||
|
* @throws @zh 名称为空、重复或超出限制时抛出错误 @en Throws if name is empty, duplicate, or limit exceeded
|
||||||
*/
|
*/
|
||||||
public createScene<T extends IScene>(sceneName: string, sceneInstance?: T): T {
|
public createScene<T extends IScene>(sceneName: string, sceneInstance?: T): T {
|
||||||
if (!sceneName || typeof sceneName !== 'string' || sceneName.trim() === '') {
|
this.validateSceneName(sceneName);
|
||||||
throw new Error('Scene name不能为空');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._scenes.has(sceneName)) {
|
const scene = sceneInstance ?? new Scene() as unknown as T;
|
||||||
throw new Error(`Scene name '${sceneName}' 已存在于World '${this.name}' 中`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._scenes.size >= this._config.maxScenes!) {
|
|
||||||
throw new Error(`World '${this.name}' 已达到最大Scene数量限制: ${this._config.maxScenes}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const scene = sceneInstance || (new Scene() as unknown as T);
|
|
||||||
|
|
||||||
if (this._config.debug) {
|
if (this._config.debug) {
|
||||||
const performanceMonitor = new PerformanceMonitor();
|
const monitor = new PerformanceMonitor();
|
||||||
performanceMonitor.enable();
|
monitor.enable();
|
||||||
scene.services.registerInstance(PerformanceMonitor, performanceMonitor);
|
scene.services.registerInstance(PerformanceMonitor, monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
(scene as { id?: string }).id = sceneName;
|
(scene as { id?: string }).id = sceneName;
|
||||||
if (!scene.name) {
|
scene.name ||= sceneName;
|
||||||
scene.name = sceneName;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._scenes.set(sceneName, scene);
|
this._scenes.set(sceneName, scene);
|
||||||
scene.initialize();
|
scene.initialize();
|
||||||
@@ -211,9 +210,7 @@ export class World {
|
|||||||
*/
|
*/
|
||||||
public removeScene(sceneName: string): boolean {
|
public removeScene(sceneName: string): boolean {
|
||||||
const scene = this._scenes.get(sceneName);
|
const scene = this._scenes.get(sceneName);
|
||||||
if (!scene) {
|
if (!scene) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._activeScenes.has(sceneName)) {
|
if (this._activeScenes.has(sceneName)) {
|
||||||
this.setSceneActive(sceneName, false);
|
this.setSceneActive(sceneName, false);
|
||||||
@@ -221,11 +218,20 @@ export class World {
|
|||||||
|
|
||||||
scene.end();
|
scene.end();
|
||||||
this._scenes.delete(sceneName);
|
this._scenes.delete(sceneName);
|
||||||
|
|
||||||
logger.info(`从World '${this.name}' 中移除Scene: ${sceneName}`);
|
logger.info(`从World '${this.name}' 中移除Scene: ${sceneName}`);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 移除所有Scene
|
||||||
|
* @en Remove all Scenes
|
||||||
|
*/
|
||||||
|
public removeAllScenes(): void {
|
||||||
|
this._scenes.forEach((_, name) => this.removeScene(name));
|
||||||
|
logger.info(`从World '${this.name}' 中移除所有Scene`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取Scene
|
* @zh 获取Scene
|
||||||
* @en Get Scene
|
* @en Get Scene
|
||||||
@@ -234,36 +240,31 @@ export class World {
|
|||||||
* @returns @zh Scene实例或null @en Scene instance or null
|
* @returns @zh Scene实例或null @en Scene instance or null
|
||||||
*/
|
*/
|
||||||
public getScene<T extends IScene>(sceneName: string): T | null {
|
public getScene<T extends IScene>(sceneName: string): T | null {
|
||||||
return this._scenes.get(sceneName) as T || null;
|
return (this._scenes.get(sceneName) as T) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有Scene ID
|
* @zh 获取所有Scene ID
|
||||||
|
* @en Get all Scene IDs
|
||||||
*/
|
*/
|
||||||
public getSceneIds(): string[] {
|
public getSceneIds(): string[] {
|
||||||
return Array.from(this._scenes.keys());
|
return Array.from(this._scenes.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有Scene
|
* @zh 获取所有Scene
|
||||||
|
* @en Get all Scenes
|
||||||
*/
|
*/
|
||||||
public getAllScenes(): IScene[] {
|
public getAllScenes(): IScene[] {
|
||||||
return Array.from(this._scenes.values());
|
return Array.from(this._scenes.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除所有Scene
|
* @zh 设置Scene激活状态
|
||||||
*/
|
* @en Set Scene active state
|
||||||
public removeAllScenes(): void {
|
*
|
||||||
const sceneNames = Array.from(this._scenes.keys());
|
* @param sceneName - @zh Scene名称 @en Scene name
|
||||||
for (const sceneName of sceneNames) {
|
* @param active - @zh 是否激活 @en Whether to activate
|
||||||
this.removeScene(sceneName);
|
|
||||||
}
|
|
||||||
logger.info(`从World '${this.name}' 中移除所有Scene`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置Scene激活状态
|
|
||||||
*/
|
*/
|
||||||
public setSceneActive(sceneName: string, active: boolean): void {
|
public setSceneActive(sceneName: string, active: boolean): void {
|
||||||
const scene = this._scenes.get(sceneName);
|
const scene = this._scenes.get(sceneName);
|
||||||
@@ -283,22 +284,27 @@ export class World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查Scene是否激活
|
* @zh 检查Scene是否激活
|
||||||
|
* @en Check if Scene is active
|
||||||
*/
|
*/
|
||||||
public isSceneActive(sceneName: string): boolean {
|
public isSceneActive(sceneName: string): boolean {
|
||||||
return this._activeScenes.has(sceneName);
|
return this._activeScenes.has(sceneName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取活跃Scene数量
|
* @zh 获取活跃Scene数量
|
||||||
|
* @en Get active Scene count
|
||||||
*/
|
*/
|
||||||
public getActiveSceneCount(): number {
|
public getActiveSceneCount(): number {
|
||||||
return this._activeScenes.size;
|
return this._activeScenes.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加全局System
|
* @zh 添加全局System
|
||||||
* 全局System会在所有激活Scene之前更新
|
* @en Add global System
|
||||||
|
*
|
||||||
|
* @param system - @zh 全局System实例 @en Global System instance
|
||||||
|
* @returns @zh 添加的System实例 @en Added System instance
|
||||||
*/
|
*/
|
||||||
public addGlobalSystem<T extends IGlobalSystem>(system: T): T {
|
public addGlobalSystem<T extends IGlobalSystem>(system: T): T {
|
||||||
if (this._globalSystems.includes(system)) {
|
if (this._globalSystems.includes(system)) {
|
||||||
@@ -307,132 +313,77 @@ export class World {
|
|||||||
|
|
||||||
this._globalSystems.push(system);
|
this._globalSystems.push(system);
|
||||||
system.initialize?.();
|
system.initialize?.();
|
||||||
|
|
||||||
logger.debug(`在World '${this.name}' 中添加全局System: ${system.name}`);
|
logger.debug(`在World '${this.name}' 中添加全局System: ${system.name}`);
|
||||||
|
|
||||||
return system;
|
return system;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除全局System
|
* @zh 移除全局System
|
||||||
|
* @en Remove global System
|
||||||
|
*
|
||||||
|
* @param system - @zh 要移除的System @en System to remove
|
||||||
|
* @returns @zh 是否成功移除 @en Whether removal was successful
|
||||||
*/
|
*/
|
||||||
public removeGlobalSystem(system: IGlobalSystem): boolean {
|
public removeGlobalSystem(system: IGlobalSystem): boolean {
|
||||||
const index = this._globalSystems.indexOf(system);
|
const index = this._globalSystems.indexOf(system);
|
||||||
if (index === -1) {
|
if (index === -1) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._globalSystems.splice(index, 1);
|
this._globalSystems.splice(index, 1);
|
||||||
system.reset?.();
|
system.reset?.();
|
||||||
|
|
||||||
logger.debug(`从World '${this.name}' 中移除全局System: ${system.name}`);
|
logger.debug(`从World '${this.name}' 中移除全局System: ${system.name}`);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取全局System
|
* @zh 获取全局System
|
||||||
|
* @en Get global System
|
||||||
|
*
|
||||||
|
* @param type - @zh System类型 @en System type
|
||||||
|
* @returns @zh System实例或null @en System instance or null
|
||||||
*/
|
*/
|
||||||
public getGlobalSystem<T extends IGlobalSystem>(type: new (...args: any[]) => T): T | null {
|
public getGlobalSystem<T extends IGlobalSystem>(type: new (...args: unknown[]) => T): T | null {
|
||||||
for (const system of this._globalSystems) {
|
return (this._globalSystems.find(s => s instanceof type) as T) ?? null;
|
||||||
if (system instanceof type) {
|
|
||||||
return system as T;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动World
|
* @zh 启动World
|
||||||
|
* @en Start World
|
||||||
*/
|
*/
|
||||||
public start(): void {
|
public start(): void {
|
||||||
if (this._isActive) {
|
if (this._isActive) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isActive = true;
|
this._isActive = true;
|
||||||
|
this._globalSystems.forEach(s => s.initialize?.());
|
||||||
for (const system of this._globalSystems) {
|
|
||||||
system.initialize?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`启动World: ${this.name}`);
|
logger.info(`启动World: ${this.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止World
|
* @zh 停止World
|
||||||
|
* @en Stop World
|
||||||
*/
|
*/
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
if (!this._isActive) {
|
if (!this._isActive) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const sceneName of this._activeScenes) {
|
|
||||||
this.setSceneActive(sceneName, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const system of this._globalSystems) {
|
|
||||||
system.reset?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
this._activeScenes.forEach(name => this.setSceneActive(name, false));
|
||||||
|
this._globalSystems.forEach(s => s.reset?.());
|
||||||
this._isActive = false;
|
this._isActive = false;
|
||||||
logger.info(`停止World: ${this.name}`);
|
logger.info(`停止World: ${this.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 更新World中的全局System
|
* @zh 销毁World
|
||||||
* @en Update global systems in World
|
* @en Destroy World
|
||||||
*
|
|
||||||
* @internal Called by Core.update()
|
|
||||||
*/
|
|
||||||
public updateGlobalSystems(): void {
|
|
||||||
if (!this._isActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const system of this._globalSystems) {
|
|
||||||
system.update?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh 更新World中的所有激活Scene
|
|
||||||
* @en Update all active scenes in World
|
|
||||||
*
|
|
||||||
* @internal Called by Core.update()
|
|
||||||
*/
|
|
||||||
public updateScenes(): void {
|
|
||||||
if (!this._isActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const sceneName of this._activeScenes) {
|
|
||||||
const scene = this._scenes.get(sceneName);
|
|
||||||
scene?.update?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._config.autoCleanup) {
|
|
||||||
this.cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁World
|
|
||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
logger.info(`销毁World: ${this.name}`);
|
logger.info(`销毁World: ${this.name}`);
|
||||||
|
|
||||||
this.stop();
|
this.stop();
|
||||||
|
this.removeAllScenes();
|
||||||
|
|
||||||
for (const sceneName of Array.from(this._scenes.keys())) {
|
this._globalSystems.forEach(s => s.destroy?.() ?? s.reset?.());
|
||||||
this.removeScene(sceneName);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const system of this._globalSystems) {
|
|
||||||
if (system.destroy) {
|
|
||||||
system.destroy();
|
|
||||||
} else {
|
|
||||||
system.reset?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._globalSystems.length = 0;
|
this._globalSystems.length = 0;
|
||||||
|
|
||||||
this._services.clear();
|
this._services.clear();
|
||||||
@@ -440,10 +391,49 @@ export class World {
|
|||||||
this._activeScenes.clear();
|
this._activeScenes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取World状态
|
* @zh 更新World中的全局System
|
||||||
|
* @en Update global systems in World
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public updateGlobalSystems(): void {
|
||||||
|
if (!this._isActive) return;
|
||||||
|
this._globalSystems.forEach(s => s.update?.());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 更新World中的所有激活Scene
|
||||||
|
* @en Update all active scenes in World
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public updateScenes(): void {
|
||||||
|
if (!this._isActive) return;
|
||||||
|
|
||||||
|
this._activeScenes.forEach(name => {
|
||||||
|
this._scenes.get(name)?.update?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this._config.autoCleanup) {
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取World状态
|
||||||
|
* @en Get World status
|
||||||
*/
|
*/
|
||||||
public getStatus() {
|
public getStatus() {
|
||||||
|
const scenes: Array<{ id: string; name: string; isActive: boolean }> = [];
|
||||||
|
this._scenes.forEach((scene, id) => {
|
||||||
|
scenes.push({
|
||||||
|
id,
|
||||||
|
name: scene.name || id,
|
||||||
|
isActive: this._activeScenes.has(id)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
isActive: this._isActive,
|
isActive: this._isActive,
|
||||||
@@ -452,46 +442,61 @@ export class World {
|
|||||||
globalSystemCount: this._globalSystems.length,
|
globalSystemCount: this._globalSystems.length,
|
||||||
createdAt: this._createdAt,
|
createdAt: this._createdAt,
|
||||||
config: { ...this._config },
|
config: { ...this._config },
|
||||||
scenes: Array.from(this._scenes.keys()).map((sceneName) => ({
|
scenes
|
||||||
id: sceneName,
|
|
||||||
isActive: this._activeScenes.has(sceneName),
|
|
||||||
name: this._scenes.get(sceneName)?.name || sceneName
|
|
||||||
}))
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取World统计信息
|
* @zh 获取World统计信息
|
||||||
|
* @en Get World statistics
|
||||||
*/
|
*/
|
||||||
public getStats() {
|
public getStats() {
|
||||||
const stats = {
|
let totalEntities = 0;
|
||||||
totalEntities: 0,
|
let totalSystems = this._globalSystems.length;
|
||||||
totalSystems: this._globalSystems.length,
|
|
||||||
|
this._scenes.forEach(scene => {
|
||||||
|
totalEntities += scene.entities?.count ?? 0;
|
||||||
|
totalSystems += scene.systems?.length ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalEntities,
|
||||||
|
totalSystems,
|
||||||
memoryUsage: 0,
|
memoryUsage: 0,
|
||||||
performance: {
|
performance: {
|
||||||
averageUpdateTime: 0,
|
averageUpdateTime: 0,
|
||||||
maxUpdateTime: 0
|
maxUpdateTime: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
for (const scene of this._scenes.values()) {
|
|
||||||
stats.totalEntities += scene.entities?.count ?? 0;
|
/**
|
||||||
stats.totalSystems += scene.systems?.length ?? 0;
|
* @zh 验证Scene名称
|
||||||
|
* @en Validate Scene name
|
||||||
|
*/
|
||||||
|
private validateSceneName(sceneName: string): void {
|
||||||
|
if (!sceneName?.trim()) {
|
||||||
|
throw new Error('Scene name不能为空');
|
||||||
|
}
|
||||||
|
if (this._scenes.has(sceneName)) {
|
||||||
|
throw new Error(`Scene name '${sceneName}' 已存在于World '${this.name}' 中`);
|
||||||
|
}
|
||||||
|
if (this._scenes.size >= this._config.maxScenes) {
|
||||||
|
throw new Error(`World '${this.name}' 已达到最大Scene数量限制: ${this._config.maxScenes}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 检查Scene是否可以被自动清理
|
* @zh 检查Scene是否可以被自动清理
|
||||||
* @en Check if a scene is eligible for auto cleanup
|
* @en Check if a scene is eligible for auto cleanup
|
||||||
*/
|
*/
|
||||||
private _isSceneCleanupCandidate(sceneName: string, scene: IScene): boolean {
|
private isCleanupCandidate(sceneName: string, scene: IScene): boolean {
|
||||||
const elapsed = Date.now() - this._createdAt;
|
const elapsed = Date.now() - this._createdAt;
|
||||||
return !this._activeScenes.has(sceneName) &&
|
return !this._activeScenes.has(sceneName) &&
|
||||||
scene.entities != null &&
|
scene.entities != null &&
|
||||||
scene.entities.count === 0 &&
|
scene.entities.count === 0 &&
|
||||||
elapsed > this._config.cleanupThresholdMs!;
|
elapsed > this._config.cleanupThresholdMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -499,33 +504,18 @@ export class World {
|
|||||||
* @en Execute auto cleanup operation
|
* @en Execute auto cleanup operation
|
||||||
*/
|
*/
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
const candidates = [...this._scenes.entries()]
|
const toRemove: string[] = [];
|
||||||
.filter(([name, scene]) => this._isSceneCleanupCandidate(name, scene));
|
|
||||||
|
|
||||||
for (const [sceneName] of candidates) {
|
this._scenes.forEach((scene, name) => {
|
||||||
this.removeScene(sceneName);
|
if (this.isCleanupCandidate(name, scene)) {
|
||||||
logger.debug(`自动清理空Scene: ${sceneName} from World ${this.name}`);
|
toRemove.push(name);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toRemove.forEach(name => {
|
||||||
|
this.removeScene(name);
|
||||||
|
logger.debug(`自动清理空Scene: ${name} from World ${this.name}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查World是否激活
|
|
||||||
*/
|
|
||||||
public get isActive(): boolean {
|
|
||||||
return this._isActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Scene数量
|
|
||||||
*/
|
|
||||||
public get sceneCount(): number {
|
|
||||||
return this._scenes.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取创建时间
|
|
||||||
*/
|
|
||||||
public get createdAt(): number {
|
|
||||||
return this._createdAt;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,80 +5,85 @@ import type { IService } from '../Core/ServiceContainer';
|
|||||||
const logger = createLogger('WorldManager');
|
const logger = createLogger('WorldManager');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WorldManager配置接口
|
* @zh WorldManager配置接口
|
||||||
|
* @en WorldManager configuration interface
|
||||||
*/
|
*/
|
||||||
export type IWorldManagerConfig = {
|
export interface IWorldManagerConfig {
|
||||||
/**
|
/**
|
||||||
* 最大World数量
|
* @zh 最大World数量
|
||||||
|
* @en Maximum number of worlds
|
||||||
*/
|
*/
|
||||||
maxWorlds?: number;
|
maxWorlds?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否自动清理空World
|
* @zh 是否自动清理空World
|
||||||
|
* @en Auto cleanup empty worlds
|
||||||
*/
|
*/
|
||||||
autoCleanup?: boolean;
|
autoCleanup?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理间隔(帧数)
|
* @zh 清理间隔(帧数)
|
||||||
|
* @en Cleanup interval in frames
|
||||||
*/
|
*/
|
||||||
cleanupFrameInterval?: number;
|
cleanupFrameInterval?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否启用调试模式
|
* @zh 是否启用调试模式
|
||||||
|
* @en Enable debug mode
|
||||||
*/
|
*/
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* World管理器 - 管理所有World实例
|
* @zh WorldManager默认配置
|
||||||
|
* @en WorldManager default configuration
|
||||||
|
*/
|
||||||
|
const DEFAULT_CONFIG: Required<IWorldManagerConfig> = {
|
||||||
|
maxWorlds: 50,
|
||||||
|
autoCleanup: true,
|
||||||
|
cleanupFrameInterval: 1800,
|
||||||
|
debug: false
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 清理阈值(毫秒)
|
||||||
|
* @en Cleanup threshold in milliseconds
|
||||||
|
*/
|
||||||
|
const CLEANUP_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh World管理器 - 管理所有World实例
|
||||||
|
* @en World Manager - Manages all World instances
|
||||||
*
|
*
|
||||||
* WorldManager负责管理多个独立的World实例。
|
* @zh WorldManager负责管理多个独立的World实例。
|
||||||
* 每个World都是独立的ECS环境,可以包含多个Scene。
|
* 每个World都是独立的ECS环境,可以包含多个Scene。
|
||||||
|
* @en WorldManager is responsible for managing multiple independent World instances.
|
||||||
|
* Each World is an isolated ECS environment that can contain multiple Scenes.
|
||||||
*
|
*
|
||||||
* 适用场景:
|
* @zh 适用场景:
|
||||||
* - MMO游戏的多房间管理
|
* - MMO游戏的多房间管理
|
||||||
* - 服务器端的多游戏实例
|
* - 服务器端的多游戏实例
|
||||||
* - 需要完全隔离的多个游戏环境
|
* - 需要完全隔离的多个游戏环境
|
||||||
|
* @en Use cases:
|
||||||
|
* - Multi-room management for MMO games
|
||||||
|
* - Multiple game instances on server-side
|
||||||
|
* - Completely isolated game environments
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // 创建WorldManager实例
|
* const worldManager = new WorldManager({ maxWorlds: 100 });
|
||||||
* const worldManager = new WorldManager({
|
* const room = worldManager.createWorld('room_001');
|
||||||
* maxWorlds: 100,
|
* worldManager.setWorldActive('room_001', true);
|
||||||
* autoCleanup: true
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // 创建游戏房间World
|
|
||||||
* const room1 = worldManager.createWorld('room_001', {
|
|
||||||
* name: 'GameRoom_001',
|
|
||||||
* maxScenes: 5
|
|
||||||
* });
|
|
||||||
* room1.setActive(true);
|
|
||||||
*
|
|
||||||
* // 游戏循环
|
|
||||||
* function gameLoop(deltaTime: number) {
|
|
||||||
* Core.update(deltaTime);
|
|
||||||
* worldManager.updateAll(); // 更新所有活跃World
|
|
||||||
* }
|
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class WorldManager implements IService {
|
export class WorldManager implements IService {
|
||||||
private readonly _config: Required<IWorldManagerConfig>;
|
private readonly _config: Required<IWorldManagerConfig>;
|
||||||
private readonly _worlds: Map<string, World> = new Map();
|
private readonly _worlds = new Map<string, World>();
|
||||||
private _isRunning: boolean = false;
|
private _isRunning = true;
|
||||||
private _framesSinceCleanup: number = 0;
|
private _framesSinceCleanup = 0;
|
||||||
|
|
||||||
public constructor(config: IWorldManagerConfig = {}) {
|
constructor(config: IWorldManagerConfig = {}) {
|
||||||
this._config = {
|
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||||
maxWorlds: 50,
|
|
||||||
autoCleanup: true,
|
|
||||||
cleanupFrameInterval: 1800, // 1800帧
|
|
||||||
debug: false,
|
|
||||||
...config
|
|
||||||
};
|
|
||||||
|
|
||||||
// 默认启动运行状态
|
|
||||||
this._isRunning = true;
|
|
||||||
|
|
||||||
logger.info('WorldManager已初始化', {
|
logger.info('WorldManager已初始化', {
|
||||||
maxWorlds: this._config.maxWorlds,
|
maxWorlds: this._config.maxWorlds,
|
||||||
@@ -88,27 +93,57 @@ export class WorldManager implements IService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建新World
|
* @zh 获取World总数
|
||||||
|
* @en Get total world count
|
||||||
|
*/
|
||||||
|
public get worldCount(): number {
|
||||||
|
return this._worlds.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取激活World数量
|
||||||
|
* @en Get active world count
|
||||||
|
*/
|
||||||
|
public get activeWorldCount(): number {
|
||||||
|
let count = 0;
|
||||||
|
this._worlds.forEach(world => {
|
||||||
|
if (world.isActive) count++;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 检查是否正在运行
|
||||||
|
* @en Check if running
|
||||||
|
*/
|
||||||
|
public get isRunning(): boolean {
|
||||||
|
return this._isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取配置
|
||||||
|
* @en Get configuration
|
||||||
|
*/
|
||||||
|
public get config(): IWorldManagerConfig {
|
||||||
|
return { ...this._config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 创建新World
|
||||||
|
* @en Create new World
|
||||||
|
*
|
||||||
|
* @param worldName - @zh World名称 @en World name
|
||||||
|
* @param config - @zh World配置 @en World configuration
|
||||||
|
* @returns @zh 创建的World实例 @en Created World instance
|
||||||
|
* @throws @zh 名称为空、重复或超出限制时抛出错误 @en Throws if name is empty, duplicate, or limit exceeded
|
||||||
*/
|
*/
|
||||||
public createWorld(worldName: string, config?: IWorldConfig): World {
|
public createWorld(worldName: string, config?: IWorldConfig): World {
|
||||||
if (!worldName || typeof worldName !== 'string' || worldName.trim() === '') {
|
this.validateWorldName(worldName);
|
||||||
throw new Error('World name不能为空');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._worlds.has(worldName)) {
|
|
||||||
throw new Error(`World name '${worldName}' 已存在`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._worlds.size >= this._config.maxWorlds!) {
|
|
||||||
throw new Error(`已达到最大World数量限制: ${this._config.maxWorlds}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先级:config.debug > WorldManager.debug > 默认
|
|
||||||
const worldConfig: IWorldConfig = {
|
const worldConfig: IWorldConfig = {
|
||||||
|
...config,
|
||||||
name: worldName,
|
name: worldName,
|
||||||
debug: config?.debug ?? this._config.debug ?? false,
|
debug: config?.debug ?? this._config.debug
|
||||||
...(config?.maxScenes !== undefined && { maxScenes: config.maxScenes }),
|
|
||||||
...(config?.autoCleanup !== undefined && { autoCleanup: config.autoCleanup })
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const world = new World(worldConfig);
|
const world = new World(worldConfig);
|
||||||
@@ -118,45 +153,56 @@ export class WorldManager implements IService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除World
|
* @zh 移除World
|
||||||
|
* @en Remove World
|
||||||
|
*
|
||||||
|
* @param worldName - @zh World名称 @en World name
|
||||||
|
* @returns @zh 是否成功移除 @en Whether removal was successful
|
||||||
*/
|
*/
|
||||||
public removeWorld(worldName: string): boolean {
|
public removeWorld(worldName: string): boolean {
|
||||||
const world = this._worlds.get(worldName);
|
const world = this._worlds.get(worldName);
|
||||||
if (!world) {
|
if (!world) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 销毁World
|
|
||||||
world.destroy();
|
world.destroy();
|
||||||
this._worlds.delete(worldName);
|
this._worlds.delete(worldName);
|
||||||
|
|
||||||
logger.info(`移除World: ${worldName}`);
|
logger.info(`移除World: ${worldName}`);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取World
|
* @zh 获取World
|
||||||
|
* @en Get World
|
||||||
|
*
|
||||||
|
* @param worldName - @zh World名称 @en World name
|
||||||
|
* @returns @zh World实例或null @en World instance or null
|
||||||
*/
|
*/
|
||||||
public getWorld(worldName: string): World | null {
|
public getWorld(worldName: string): World | null {
|
||||||
return this._worlds.get(worldName) || null;
|
return this._worlds.get(worldName) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有World ID
|
* @zh 获取所有World ID
|
||||||
|
* @en Get all World IDs
|
||||||
*/
|
*/
|
||||||
public getWorldIds(): string[] {
|
public getWorldIds(): string[] {
|
||||||
return Array.from(this._worlds.keys());
|
return Array.from(this._worlds.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有World
|
* @zh 获取所有World
|
||||||
|
* @en Get all Worlds
|
||||||
*/
|
*/
|
||||||
public getAllWorlds(): World[] {
|
public getAllWorlds(): World[] {
|
||||||
return Array.from(this._worlds.values());
|
return Array.from(this._worlds.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置World激活状态
|
* @zh 设置World激活状态
|
||||||
|
* @en Set World active state
|
||||||
|
*
|
||||||
|
* @param worldName - @zh World名称 @en World name
|
||||||
|
* @param active - @zh 是否激活 @en Whether to activate
|
||||||
*/
|
*/
|
||||||
public setWorldActive(worldName: string, active: boolean): void {
|
public setWorldActive(worldName: string, active: boolean): void {
|
||||||
const world = this._worlds.get(worldName);
|
const world = this._worlds.get(worldName);
|
||||||
@@ -175,204 +221,84 @@ export class WorldManager implements IService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查World是否激活
|
* @zh 检查World是否激活
|
||||||
|
* @en Check if World is active
|
||||||
*/
|
*/
|
||||||
public isWorldActive(worldName: string): boolean {
|
public isWorldActive(worldName: string): boolean {
|
||||||
const world = this._worlds.get(worldName);
|
return this._worlds.get(worldName)?.isActive ?? false;
|
||||||
return world?.isActive ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新所有活跃的World
|
* @zh 获取所有激活的World
|
||||||
*
|
* @en Get all active Worlds
|
||||||
* 应该在每帧的游戏循环中调用。
|
|
||||||
* 会自动更新所有活跃World的全局系统和场景。
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* function gameLoop(deltaTime: number) {
|
|
||||||
* Core.update(deltaTime); // 更新全局服务
|
|
||||||
* worldManager.updateAll(); // 更新所有World
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public updateAll(): void {
|
|
||||||
if (!this._isRunning) return;
|
|
||||||
|
|
||||||
for (const world of this._worlds.values()) {
|
|
||||||
if (world.isActive) {
|
|
||||||
// 更新World的全局System
|
|
||||||
world.updateGlobalSystems();
|
|
||||||
|
|
||||||
// 更新World中的所有Scene
|
|
||||||
world.updateScenes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 基于帧的自动清理
|
|
||||||
if (this._config.autoCleanup) {
|
|
||||||
this._framesSinceCleanup++;
|
|
||||||
|
|
||||||
if (this._framesSinceCleanup >= this._config.cleanupFrameInterval) {
|
|
||||||
this.cleanup();
|
|
||||||
this._framesSinceCleanup = 0; // 重置计数器
|
|
||||||
|
|
||||||
if (this._config.debug) {
|
|
||||||
logger.debug(`执行定期清理World (间隔: ${this._config.cleanupFrameInterval} 帧)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有激活的World
|
|
||||||
*/
|
*/
|
||||||
public getActiveWorlds(): World[] {
|
public getActiveWorlds(): World[] {
|
||||||
const activeWorlds: World[] = [];
|
const result: World[] = [];
|
||||||
for (const world of this._worlds.values()) {
|
this._worlds.forEach(world => {
|
||||||
if (world.isActive) {
|
if (world.isActive) result.push(world);
|
||||||
activeWorlds.push(world);
|
});
|
||||||
}
|
return result;
|
||||||
}
|
|
||||||
return activeWorlds;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动所有World
|
* @zh 查找满足条件的World
|
||||||
|
* @en Find Worlds matching predicate
|
||||||
|
*
|
||||||
|
* @param predicate - @zh 过滤条件 @en Filter predicate
|
||||||
|
*/
|
||||||
|
public findWorlds(predicate: (world: World) => boolean): World[] {
|
||||||
|
const result: World[] = [];
|
||||||
|
this._worlds.forEach(world => {
|
||||||
|
if (predicate(world)) result.push(world);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 根据名称查找World
|
||||||
|
* @en Find World by name
|
||||||
|
*
|
||||||
|
* @param name - @zh World名称 @en World name
|
||||||
|
*/
|
||||||
|
public findWorldByName(name: string): World | null {
|
||||||
|
let found: World | null = null;
|
||||||
|
this._worlds.forEach(world => {
|
||||||
|
if (world.name === name) found = world;
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 启动所有World
|
||||||
|
* @en Start all Worlds
|
||||||
*/
|
*/
|
||||||
public startAll(): void {
|
public startAll(): void {
|
||||||
this._isRunning = true;
|
this._isRunning = true;
|
||||||
|
this._worlds.forEach(world => world.start());
|
||||||
for (const world of this._worlds.values()) {
|
|
||||||
world.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('启动所有World');
|
logger.info('启动所有World');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止所有World
|
* @zh 停止所有World
|
||||||
|
* @en Stop all Worlds
|
||||||
*/
|
*/
|
||||||
public stopAll(): void {
|
public stopAll(): void {
|
||||||
this._isRunning = false;
|
this._isRunning = false;
|
||||||
|
this._worlds.forEach(world => world.stop());
|
||||||
for (const world of this._worlds.values()) {
|
|
||||||
world.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('停止所有World');
|
logger.info('停止所有World');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找满足条件的World
|
* @zh 销毁WorldManager
|
||||||
*/
|
* @en Destroy WorldManager
|
||||||
public findWorlds(predicate: (world: World) => boolean): World[] {
|
|
||||||
const results: World[] = [];
|
|
||||||
for (const world of this._worlds.values()) {
|
|
||||||
if (predicate(world)) {
|
|
||||||
results.push(world);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据名称查找World
|
|
||||||
*/
|
|
||||||
public findWorldByName(name: string): World | null {
|
|
||||||
for (const world of this._worlds.values()) {
|
|
||||||
if (world.name === name) {
|
|
||||||
return world;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取WorldManager统计信息
|
|
||||||
*/
|
|
||||||
public getStats() {
|
|
||||||
const stats = {
|
|
||||||
totalWorlds: this._worlds.size,
|
|
||||||
activeWorlds: this.activeWorldCount,
|
|
||||||
totalScenes: 0,
|
|
||||||
totalEntities: 0,
|
|
||||||
totalSystems: 0,
|
|
||||||
memoryUsage: 0,
|
|
||||||
isRunning: this._isRunning,
|
|
||||||
config: { ...this._config },
|
|
||||||
worlds: [] as any[]
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [worldName, world] of this._worlds) {
|
|
||||||
const worldStats = world.getStats();
|
|
||||||
stats.totalScenes += worldStats.totalSystems; // World的getStats可能需要调整
|
|
||||||
stats.totalEntities += worldStats.totalEntities;
|
|
||||||
stats.totalSystems += worldStats.totalSystems;
|
|
||||||
|
|
||||||
stats.worlds.push({
|
|
||||||
id: worldName,
|
|
||||||
name: world.name,
|
|
||||||
isActive: world.isActive,
|
|
||||||
sceneCount: world.sceneCount,
|
|
||||||
...worldStats
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取详细状态信息
|
|
||||||
*/
|
|
||||||
public getDetailedStatus() {
|
|
||||||
return {
|
|
||||||
...this.getStats(),
|
|
||||||
worlds: Array.from(this._worlds.entries()).map(([worldName, world]) => ({
|
|
||||||
id: worldName,
|
|
||||||
isActive: world.isActive,
|
|
||||||
status: world.getStatus()
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理空World
|
|
||||||
*/
|
|
||||||
public cleanup(): number {
|
|
||||||
const worldsToRemove: string[] = [];
|
|
||||||
|
|
||||||
for (const [worldName, world] of this._worlds) {
|
|
||||||
if (this.shouldCleanupWorld(world)) {
|
|
||||||
worldsToRemove.push(worldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const worldName of worldsToRemove) {
|
|
||||||
this.removeWorld(worldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (worldsToRemove.length > 0) {
|
|
||||||
logger.debug(`清理了 ${worldsToRemove.length} 个World`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return worldsToRemove.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁WorldManager
|
|
||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
logger.info('正在销毁WorldManager...');
|
logger.info('正在销毁WorldManager...');
|
||||||
|
|
||||||
// 停止所有World
|
|
||||||
this.stopAll();
|
this.stopAll();
|
||||||
|
|
||||||
// 销毁所有World
|
|
||||||
const worldNames = Array.from(this._worlds.keys());
|
const worldNames = Array.from(this._worlds.keys());
|
||||||
for (const worldName of worldNames) {
|
worldNames.forEach(name => this.removeWorld(name));
|
||||||
this.removeWorld(worldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._worlds.clear();
|
this._worlds.clear();
|
||||||
this._isRunning = false;
|
this._isRunning = false;
|
||||||
@@ -381,67 +307,178 @@ export class WorldManager implements IService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实现 IService 接口的 dispose 方法
|
* @zh 实现 IService 接口的 dispose 方法
|
||||||
* 调用 destroy 方法进行清理
|
* @en Implement IService dispose method
|
||||||
*/
|
*/
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断World是否应该被清理
|
* @zh 更新所有活跃的World
|
||||||
* 清理策略:
|
* @en Update all active Worlds
|
||||||
* 1. World未激活
|
*
|
||||||
* 2. 没有Scene或所有Scene都是空的
|
* @zh 应该在每帧的游戏循环中调用
|
||||||
* 3. 创建时间超过10分钟
|
* @en Should be called in each frame of game loop
|
||||||
*/
|
*/
|
||||||
private shouldCleanupWorld(world: World): boolean {
|
public updateAll(): void {
|
||||||
if (world.isActive) {
|
if (!this._isRunning) return;
|
||||||
return false;
|
|
||||||
|
this._worlds.forEach(world => {
|
||||||
|
if (world.isActive) {
|
||||||
|
world.updateGlobalSystems();
|
||||||
|
world.updateScenes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.processAutoCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取WorldManager统计信息
|
||||||
|
* @en Get WorldManager statistics
|
||||||
|
*/
|
||||||
|
public getStats() {
|
||||||
|
let totalScenes = 0;
|
||||||
|
let totalEntities = 0;
|
||||||
|
let totalSystems = 0;
|
||||||
|
const worldsList: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isActive: boolean;
|
||||||
|
sceneCount: number;
|
||||||
|
totalEntities: number;
|
||||||
|
totalSystems: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
this._worlds.forEach((world, worldName) => {
|
||||||
|
const worldStats = world.getStats();
|
||||||
|
totalScenes += world.sceneCount;
|
||||||
|
totalEntities += worldStats.totalEntities;
|
||||||
|
totalSystems += worldStats.totalSystems;
|
||||||
|
|
||||||
|
worldsList.push({
|
||||||
|
id: worldName,
|
||||||
|
name: world.name,
|
||||||
|
isActive: world.isActive,
|
||||||
|
sceneCount: world.sceneCount,
|
||||||
|
...worldStats
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalWorlds: this._worlds.size,
|
||||||
|
activeWorlds: this.activeWorldCount,
|
||||||
|
totalScenes,
|
||||||
|
totalEntities,
|
||||||
|
totalSystems,
|
||||||
|
memoryUsage: 0,
|
||||||
|
isRunning: this._isRunning,
|
||||||
|
config: { ...this._config },
|
||||||
|
worlds: worldsList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取详细状态信息
|
||||||
|
* @en Get detailed status information
|
||||||
|
*/
|
||||||
|
public getDetailedStatus() {
|
||||||
|
const worlds: Array<{
|
||||||
|
id: string;
|
||||||
|
isActive: boolean;
|
||||||
|
status: ReturnType<World['getStatus']>;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
this._worlds.forEach((world, worldName) => {
|
||||||
|
worlds.push({
|
||||||
|
id: worldName,
|
||||||
|
isActive: world.isActive,
|
||||||
|
status: world.getStatus()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...this.getStats(), worlds };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 清理空World
|
||||||
|
* @en Cleanup empty Worlds
|
||||||
|
*
|
||||||
|
* @returns @zh 清理的World数量 @en Number of cleaned up Worlds
|
||||||
|
*/
|
||||||
|
public cleanup(): number {
|
||||||
|
const toRemove: string[] = [];
|
||||||
|
|
||||||
|
this._worlds.forEach((world, worldName) => {
|
||||||
|
if (this.isCleanupCandidate(world)) {
|
||||||
|
toRemove.push(worldName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toRemove.forEach(name => this.removeWorld(name));
|
||||||
|
|
||||||
|
if (toRemove.length > 0) {
|
||||||
|
logger.debug(`清理了 ${toRemove.length} 个World`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return toRemove.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 验证World名称
|
||||||
|
* @en Validate World name
|
||||||
|
*/
|
||||||
|
private validateWorldName(worldName: string): void {
|
||||||
|
if (!worldName?.trim()) {
|
||||||
|
throw new Error('World name不能为空');
|
||||||
|
}
|
||||||
|
if (this._worlds.has(worldName)) {
|
||||||
|
throw new Error(`World name '${worldName}' 已存在`);
|
||||||
|
}
|
||||||
|
if (this._worlds.size >= this._config.maxWorlds) {
|
||||||
|
throw new Error(`已达到最大World数量限制: ${this._config.maxWorlds}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 处理自动清理
|
||||||
|
* @en Process auto cleanup
|
||||||
|
*/
|
||||||
|
private processAutoCleanup(): void {
|
||||||
|
if (!this._config.autoCleanup) return;
|
||||||
|
|
||||||
|
this._framesSinceCleanup++;
|
||||||
|
|
||||||
|
if (this._framesSinceCleanup >= this._config.cleanupFrameInterval) {
|
||||||
|
this.cleanup();
|
||||||
|
this._framesSinceCleanup = 0;
|
||||||
|
|
||||||
|
if (this._config.debug) {
|
||||||
|
logger.debug(`执行定期清理World (间隔: ${this._config.cleanupFrameInterval} 帧)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 判断World是否应该被清理
|
||||||
|
* @en Check if World should be cleaned up
|
||||||
|
*
|
||||||
|
* @zh 清理策略:未激活 + (无Scene或全空Scene) + 创建超过10分钟
|
||||||
|
* @en Cleanup policy: inactive + (no scenes or all empty) + created over 10 minutes ago
|
||||||
|
*/
|
||||||
|
private isCleanupCandidate(world: World): boolean {
|
||||||
|
if (world.isActive) return false;
|
||||||
|
|
||||||
const age = Date.now() - world.createdAt;
|
const age = Date.now() - world.createdAt;
|
||||||
const isOldEnough = age > 10 * 60 * 1000; // 10分钟
|
if (age <= CLEANUP_THRESHOLD_MS) return false;
|
||||||
|
|
||||||
if (world.sceneCount === 0) {
|
if (world.sceneCount === 0) return true;
|
||||||
return isOldEnough;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否所有Scene都是空的
|
const hasEntities = world.getAllScenes().some(
|
||||||
const allScenes = world.getAllScenes();
|
scene => scene.entities && scene.entities.count > 0
|
||||||
const hasEntities = allScenes.some((scene) => scene.entities && scene.entities.count > 0);
|
);
|
||||||
return !hasEntities && isOldEnough;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return !hasEntities;
|
||||||
* 获取World总数
|
|
||||||
*/
|
|
||||||
public get worldCount(): number {
|
|
||||||
return this._worlds.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取激活World数量
|
|
||||||
*/
|
|
||||||
public get activeWorldCount(): number {
|
|
||||||
let count = 0;
|
|
||||||
for (const world of this._worlds.values()) {
|
|
||||||
if (world.isActive) count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否正在运行
|
|
||||||
*/
|
|
||||||
public get isRunning(): boolean {
|
|
||||||
return this._isRunning;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取配置
|
|
||||||
*/
|
|
||||||
public get config(): IWorldManagerConfig {
|
|
||||||
return { ...this._config };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# @esengine/fsm
|
# @esengine/fsm
|
||||||
|
|
||||||
|
## 1.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
|
||||||
|
- @esengine/ecs-framework@2.4.4
|
||||||
|
- @esengine/blueprint@1.0.2
|
||||||
|
|
||||||
|
## 1.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
|
||||||
|
- @esengine/ecs-framework@2.4.3
|
||||||
|
- @esengine/blueprint@1.0.1
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/fsm",
|
"name": "@esengine/fsm",
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"description": "Finite State Machine for ECS Framework / ECS 框架的有限状态机",
|
"description": "Finite State Machine for ECS Framework / ECS 框架的有限状态机",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# @esengine/pathfinding
|
# @esengine/pathfinding
|
||||||
|
|
||||||
|
## 1.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
|
||||||
|
- @esengine/ecs-framework@2.4.4
|
||||||
|
- @esengine/blueprint@1.0.2
|
||||||
|
|
||||||
|
## 1.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
|
||||||
|
- @esengine/ecs-framework@2.4.3
|
||||||
|
- @esengine/blueprint@1.0.1
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/pathfinding",
|
"name": "@esengine/pathfinding",
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"description": "寻路系统 | Pathfinding System - A*, Grid, NavMesh",
|
"description": "寻路系统 | Pathfinding System - A*, Grid, NavMesh",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# @esengine/procgen
|
# @esengine/procgen
|
||||||
|
|
||||||
|
## 1.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
|
||||||
|
- @esengine/ecs-framework@2.4.4
|
||||||
|
- @esengine/blueprint@1.0.2
|
||||||
|
|
||||||
|
## 1.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
|
||||||
|
- @esengine/ecs-framework@2.4.3
|
||||||
|
- @esengine/blueprint@1.0.1
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/procgen",
|
"name": "@esengine/procgen",
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"description": "Procedural generation tools for ECS Framework / ECS 框架的程序化生成工具",
|
"description": "Procedural generation tools for ECS Framework / ECS 框架的程序化生成工具",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# @esengine/spatial
|
# @esengine/spatial
|
||||||
|
|
||||||
|
## 1.0.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
|
||||||
|
- @esengine/ecs-framework@2.4.4
|
||||||
|
- @esengine/blueprint@1.0.2
|
||||||
|
|
||||||
|
## 1.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
|
||||||
|
- @esengine/ecs-framework@2.4.3
|
||||||
|
- @esengine/blueprint@1.0.1
|
||||||
|
|
||||||
## 1.0.2
|
## 1.0.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/spatial",
|
"name": "@esengine/spatial",
|
||||||
"version": "1.0.2",
|
"version": "1.0.4",
|
||||||
"description": "Spatial query and indexing system for ECS Framework / ECS 框架的空间查询和索引系统",
|
"description": "Spatial query and indexing system for ECS Framework / ECS 框架的空间查询和索引系统",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# @esengine/timer
|
# @esengine/timer
|
||||||
|
|
||||||
|
## 1.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`7d74623`](https://github.com/esengine/esengine/commit/7d746237100084ac3456f1af92ff664db4e50cc8)]:
|
||||||
|
- @esengine/ecs-framework@2.4.4
|
||||||
|
- @esengine/blueprint@1.0.2
|
||||||
|
|
||||||
|
## 1.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`ce2db4e`](https://github.com/esengine/esengine/commit/ce2db4e48a7cdac44265420ef16e83f6424f4dea)]:
|
||||||
|
- @esengine/ecs-framework@2.4.3
|
||||||
|
- @esengine/blueprint@1.0.1
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/timer",
|
"name": "@esengine/timer",
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"description": "Timer and cooldown system for ECS Framework / ECS 框架的定时器和冷却系统",
|
"description": "Timer and cooldown system for ECS Framework / ECS 框架的定时器和冷却系统",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
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",
|
"name": "@esengine/network-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"description": "TSRPC-based network server for ESEngine",
|
"description": "TSRPC-based network server for ESEngine",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.cjs",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/index.js",
|
"import": {
|
||||||
"types": "./dist/index.d.ts"
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"default": "./dist/index.cjs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -22,7 +28,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@esengine/network-protocols": "workspace:*",
|
"@esengine/network-protocols": "workspace:*",
|
||||||
"tsrpc": "^3.4.15"
|
"tsrpc": "^3.4.15",
|
||||||
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsup": "^8.5.1",
|
"tsup": "^8.5.1",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineConfig } from 'tsup';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: ['src/index.ts', 'src/main.ts'],
|
entry: ['src/index.ts', 'src/main.ts'],
|
||||||
format: ['esm'],
|
format: ['esm', 'cjs'],
|
||||||
dts: true,
|
dts: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
clean: true,
|
clean: true,
|
||||||
|
|||||||
@@ -1,5 +1,30 @@
|
|||||||
# @esengine/cli
|
# @esengine/cli
|
||||||
|
|
||||||
|
## 1.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#354](https://github.com/esengine/esengine/pull/354) [`1e240e8`](https://github.com/esengine/esengine/commit/1e240e86f2f75672c3609c9d86238a9ec08ebb4e) Thanks [@esengine](https://github.com/esengine)! - feat(cli): 增强 Node.js 服务端适配器
|
||||||
|
|
||||||
|
**@esengine/cli:**
|
||||||
|
- 添加 @esengine/network-server 依赖支持
|
||||||
|
- 生成完整的 ECS 游戏服务器项目结构
|
||||||
|
- 组件使用 @ECSComponent 装饰器注册
|
||||||
|
- tsconfig 启用 experimentalDecorators
|
||||||
|
|
||||||
|
**@esengine/network-server:**
|
||||||
|
- 支持 ESM/CJS 双格式导出
|
||||||
|
- 添加 ws@8.18.0 解决 Node.js 24 兼容性问题
|
||||||
|
|
||||||
|
## 1.2.1
|
||||||
|
|
||||||
|
### 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.3.0",
|
||||||
"description": "CLI tool for adding ESEngine ECS framework to existing projects",
|
"description": "CLI tool for adding ESEngine ECS framework to existing projects",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import type { FileEntry, PlatformAdapter, ProjectConfig } from './types.js';
|
|||||||
export const nodejsAdapter: PlatformAdapter = {
|
export const nodejsAdapter: PlatformAdapter = {
|
||||||
id: 'nodejs',
|
id: 'nodejs',
|
||||||
name: 'Node.js',
|
name: 'Node.js',
|
||||||
description: 'Generate standalone Node.js project with ECS (for servers, CLI tools, simulations)',
|
description: 'Generate Node.js game server with ECS and networking',
|
||||||
|
|
||||||
getDependencies() {
|
getDependencies() {
|
||||||
return {
|
return {
|
||||||
'@esengine/ecs-framework': 'latest'
|
'@esengine/ecs-framework': 'latest',
|
||||||
|
'@esengine/network-server': 'latest'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -33,147 +34,96 @@ export const nodejsAdapter: PlatformAdapter = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
generateFiles(config: ProjectConfig): FileEntry[] {
|
generateFiles(config: ProjectConfig): FileEntry[] {
|
||||||
const files: FileEntry[] = [];
|
return [
|
||||||
|
{ path: 'src/index.ts', content: generateIndex(config) },
|
||||||
files.push({
|
{ path: 'src/server/GameServer.ts', content: generateGameServer(config) },
|
||||||
path: 'src/index.ts',
|
{ path: 'src/game/Game.ts', content: generateGame(config) },
|
||||||
content: generateIndex(config)
|
{ path: 'src/game/scenes/MainScene.ts', content: generateMainScene(config) },
|
||||||
});
|
{ path: 'src/game/components/PositionComponent.ts', content: generatePositionComponent() },
|
||||||
|
{ path: 'src/game/components/VelocityComponent.ts', content: generateVelocityComponent() },
|
||||||
files.push({
|
{ path: 'src/game/systems/MovementSystem.ts', content: generateMovementSystem() },
|
||||||
path: 'src/Game.ts',
|
{ path: 'tsconfig.json', content: generateTsConfig() },
|
||||||
content: generateGame(config)
|
{ path: 'README.md', content: generateReadme(config) }
|
||||||
});
|
];
|
||||||
|
|
||||||
files.push({
|
|
||||||
path: 'src/components/PositionComponent.ts',
|
|
||||||
content: generatePositionComponent()
|
|
||||||
});
|
|
||||||
|
|
||||||
files.push({
|
|
||||||
path: 'src/systems/MovementSystem.ts',
|
|
||||||
content: generateMovementSystem()
|
|
||||||
});
|
|
||||||
|
|
||||||
files.push({
|
|
||||||
path: 'tsconfig.json',
|
|
||||||
content: generateTsConfig()
|
|
||||||
});
|
|
||||||
|
|
||||||
files.push({
|
|
||||||
path: 'README.md',
|
|
||||||
content: generateReadme(config)
|
|
||||||
});
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function generateIndex(config: ProjectConfig): string {
|
function generateIndex(config: ProjectConfig): string {
|
||||||
return `import { Game } from './Game.js';
|
return `import { createGameServer } from './server/GameServer';
|
||||||
|
|
||||||
const game = new Game();
|
const PORT = Number(process.env.PORT) || 3000;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const server = createGameServer({ port: PORT });
|
||||||
|
await server.start();
|
||||||
|
|
||||||
|
console.log('========================================');
|
||||||
|
console.log(' ${config.name} Server');
|
||||||
|
console.log('========================================');
|
||||||
|
console.log(\` WebSocket: ws://localhost:\${PORT}\`);
|
||||||
|
console.log(' Press Ctrl+C to stop');
|
||||||
|
console.log('========================================');
|
||||||
|
}
|
||||||
|
|
||||||
// Handle graceful shutdown
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.log('\\nShutting down...');
|
console.log('\\nShutting down...');
|
||||||
game.stop();
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
main().catch(console.error);
|
||||||
game.stop();
|
`;
|
||||||
process.exit(0);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Start the game
|
function generateGameServer(config: ProjectConfig): string {
|
||||||
game.start();
|
return `import { GameServer, type IServerConfig } from '@esengine/network-server';
|
||||||
|
import { Game } from '../game/Game';
|
||||||
|
|
||||||
console.log('[${config.name}] Game started. Press Ctrl+C to stop.');
|
/**
|
||||||
|
* @zh 创建游戏服务器
|
||||||
|
* @en Create game server
|
||||||
|
*/
|
||||||
|
export function createGameServer(config: Partial<IServerConfig> = {}): GameServer {
|
||||||
|
const server = new GameServer({
|
||||||
|
port: config.port ?? 3000,
|
||||||
|
roomConfig: {
|
||||||
|
maxPlayers: 16,
|
||||||
|
tickRate: 20,
|
||||||
|
...config.roomConfig
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化 ECS 游戏逻辑
|
||||||
|
const game = new Game();
|
||||||
|
game.start();
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateGame(config: ProjectConfig): string {
|
function generateGame(config: ProjectConfig): string {
|
||||||
return `import { Core, Scene, type ICoreConfig } from '@esengine/ecs-framework';
|
return `import { Core, type ICoreConfig } from '@esengine/ecs-framework';
|
||||||
import { MovementSystem } from './systems/MovementSystem.js';
|
import { MainScene } from './scenes/MainScene';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Game configuration options
|
* @zh 游戏主类
|
||||||
*/
|
* @en Main game class
|
||||||
export interface GameOptions {
|
|
||||||
/** @zh 调试模式 @en Debug mode */
|
|
||||||
debug?: boolean;
|
|
||||||
/** @zh 目标帧率 @en Target FPS */
|
|
||||||
targetFPS?: number;
|
|
||||||
/** @zh 远程调试配置 @en Remote debug configuration */
|
|
||||||
remoteDebug?: {
|
|
||||||
/** @zh 启用远程调试 @en Enable remote debugging */
|
|
||||||
enabled: boolean;
|
|
||||||
/** @zh WebSocket地址 @en WebSocket URL */
|
|
||||||
url: string;
|
|
||||||
/** @zh 自动重连 @en Auto reconnect */
|
|
||||||
autoReconnect?: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Game Scene - Define your game systems here
|
|
||||||
*/
|
|
||||||
class GameScene extends Scene {
|
|
||||||
initialize(): void {
|
|
||||||
this.name = '${config.name}';
|
|
||||||
this.addSystem(new MovementSystem());
|
|
||||||
// Add more systems here...
|
|
||||||
}
|
|
||||||
|
|
||||||
onStart(): void {
|
|
||||||
// Create your initial entities here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main game class with ECS game loop
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Configurable debug mode and FPS
|
|
||||||
* - Remote debugging via WebSocket
|
|
||||||
* - Fixed timestep game loop
|
|
||||||
* - Graceful start/stop
|
|
||||||
*/
|
*/
|
||||||
export class Game {
|
export class Game {
|
||||||
private readonly _scene: GameScene;
|
private _scene: MainScene;
|
||||||
private readonly _targetFPS: number;
|
|
||||||
private _running = false;
|
private _running = false;
|
||||||
private _tickInterval: ReturnType<typeof setInterval> | null = null;
|
private _tickInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
private _lastTime = 0;
|
private _lastTime = 0;
|
||||||
|
private _targetFPS = 60;
|
||||||
|
|
||||||
get scene() { return this._scene; }
|
constructor(options: { debug?: boolean; targetFPS?: number } = {}) {
|
||||||
get running() { return this._running; }
|
const { debug = false, targetFPS = 60 } = options;
|
||||||
|
|
||||||
constructor(options: GameOptions = {}) {
|
|
||||||
const { debug = false, targetFPS = 60, remoteDebug } = options;
|
|
||||||
this._targetFPS = targetFPS;
|
this._targetFPS = targetFPS;
|
||||||
|
|
||||||
const config: ICoreConfig = { debug };
|
const config: ICoreConfig = { debug };
|
||||||
|
|
||||||
// 配置远程调试
|
|
||||||
if (remoteDebug?.enabled && remoteDebug.url) {
|
|
||||||
config.debugConfig = {
|
|
||||||
enabled: true,
|
|
||||||
websocketUrl: remoteDebug.url,
|
|
||||||
autoReconnect: remoteDebug.autoReconnect ?? true,
|
|
||||||
channels: {
|
|
||||||
entities: true,
|
|
||||||
systems: true,
|
|
||||||
performance: true,
|
|
||||||
components: true,
|
|
||||||
scenes: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Core.create(config);
|
Core.create(config);
|
||||||
this._scene = new GameScene();
|
|
||||||
|
this._scene = new MainScene();
|
||||||
Core.setScene(this._scene);
|
Core.setScene(this._scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,11 +153,38 @@ export class Game {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateMainScene(config: ProjectConfig): string {
|
||||||
|
return `import { Scene } from '@esengine/ecs-framework';
|
||||||
|
import { MovementSystem } from '../systems/MovementSystem';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 主场景
|
||||||
|
* @en Main scene
|
||||||
|
*/
|
||||||
|
export class MainScene extends Scene {
|
||||||
|
initialize(): void {
|
||||||
|
this.name = '${config.name}';
|
||||||
|
|
||||||
|
// 注册系统
|
||||||
|
this.addSystem(new MovementSystem());
|
||||||
|
|
||||||
|
// 添加更多系统...
|
||||||
|
}
|
||||||
|
|
||||||
|
onStart(): void {
|
||||||
|
// 创建初始实体
|
||||||
|
console.log('[MainScene] Scene started');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function generatePositionComponent(): string {
|
function generatePositionComponent(): string {
|
||||||
return `import { Component, ECSComponent } from '@esengine/ecs-framework';
|
return `import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Position component - stores entity position
|
* @zh 位置组件
|
||||||
|
* @en Position component
|
||||||
*/
|
*/
|
||||||
@ECSComponent('Position')
|
@ECSComponent('Position')
|
||||||
export class PositionComponent extends Component {
|
export class PositionComponent extends Component {
|
||||||
@@ -219,31 +196,61 @@ export class PositionComponent extends Component {
|
|||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.x = 0;
|
||||||
|
this.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateVelocityComponent(): string {
|
||||||
|
return `import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 速度组件
|
||||||
|
* @en Velocity component
|
||||||
|
*/
|
||||||
|
@ECSComponent('Velocity')
|
||||||
|
export class VelocityComponent extends Component {
|
||||||
|
vx = 0;
|
||||||
|
vy = 0;
|
||||||
|
|
||||||
|
constructor(vx = 0, vy = 0) {
|
||||||
|
super();
|
||||||
|
this.vx = vx;
|
||||||
|
this.vy = vy;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.vx = 0;
|
||||||
|
this.vy = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateMovementSystem(): string {
|
function generateMovementSystem(): string {
|
||||||
return `import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
|
return `import { EntitySystem, Matcher, Entity, Time } from '@esengine/ecs-framework';
|
||||||
import { PositionComponent } from '../components/PositionComponent.js';
|
import { PositionComponent } from '../components/PositionComponent';
|
||||||
|
import { VelocityComponent } from '../components/VelocityComponent';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Movement system - processes entities with PositionComponent
|
* @zh 移动系统
|
||||||
*
|
* @en Movement system
|
||||||
* Customize this system for your game logic.
|
|
||||||
*/
|
*/
|
||||||
@ECSSystem('MovementSystem')
|
|
||||||
export class MovementSystem extends EntitySystem {
|
export class MovementSystem extends EntitySystem {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Matcher.empty().all(PositionComponent));
|
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected process(entities: readonly Entity[]): void {
|
protected processEntity(entity: Entity, dt: number): void {
|
||||||
for (const entity of entities) {
|
const pos = entity.getComponent(PositionComponent)!;
|
||||||
const position = entity.getComponent(PositionComponent)!;
|
const vel = entity.getComponent(VelocityComponent)!;
|
||||||
// Update position using Time.deltaTime
|
|
||||||
// position.x += velocity.dx * Time.deltaTime;
|
pos.x += vel.vx * dt;
|
||||||
}
|
pos.y += vel.vy * dt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -253,8 +260,8 @@ function generateTsConfig(): string {
|
|||||||
return `{
|
return `{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "CommonJS",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "Node",
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
@@ -263,7 +270,9 @@ function generateTsConfig(): string {
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
@@ -274,75 +283,54 @@ function generateTsConfig(): string {
|
|||||||
function generateReadme(config: ProjectConfig): string {
|
function generateReadme(config: ProjectConfig): string {
|
||||||
return `# ${config.name}
|
return `# ${config.name}
|
||||||
|
|
||||||
A Node.js project using ESEngine ECS framework.
|
Node.js 游戏服务器,基于 ESEngine ECS 框架。
|
||||||
|
|
||||||
## Quick Start
|
## 快速开始
|
||||||
|
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
# Install dependencies
|
# 安装依赖
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Run in development mode (with hot reload)
|
# 开发模式(热重载)
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# Build and run
|
# 构建并运行
|
||||||
npm run build:start
|
npm run build:start
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## Project Structure
|
## 项目结构
|
||||||
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
src/
|
src/
|
||||||
├── index.ts # Entry point
|
├── index.ts # 入口文件
|
||||||
├── Game.ts # Game loop and ECS setup
|
├── server/
|
||||||
├── components/ # ECS components (data)
|
│ └── GameServer.ts # 网络服务器配置
|
||||||
│ └── PositionComponent.ts
|
└── game/
|
||||||
└── systems/ # ECS systems (logic)
|
├── Game.ts # ECS 游戏主类
|
||||||
└── MovementSystem.ts
|
├── scenes/
|
||||||
|
│ └── MainScene.ts # 主场景
|
||||||
|
├── components/ # ECS 组件
|
||||||
|
│ ├── PositionComponent.ts
|
||||||
|
│ └── VelocityComponent.ts
|
||||||
|
└── systems/ # ECS 系统
|
||||||
|
└── MovementSystem.ts
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## Creating Components
|
## 客户端连接
|
||||||
|
|
||||||
\`\`\`typescript
|
\`\`\`typescript
|
||||||
import { Component } from '@esengine/ecs-framework';
|
import { NetworkPlugin } from '@esengine/network';
|
||||||
|
|
||||||
export class HealthComponent extends Component {
|
const networkPlugin = new NetworkPlugin({
|
||||||
current = 100;
|
serverUrl: 'ws://localhost:3000'
|
||||||
max = 100;
|
});
|
||||||
|
|
||||||
reset(): void {
|
await networkPlugin.connect('PlayerName');
|
||||||
this.current = this.max;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## Creating Systems
|
## 文档
|
||||||
|
|
||||||
\`\`\`typescript
|
- [ESEngine 文档](https://esengine.github.io/esengine/)
|
||||||
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
- [Network 模块](https://esengine.github.io/esengine/modules/network/)
|
||||||
import { HealthComponent } from '../components/HealthComponent.js';
|
|
||||||
|
|
||||||
export class HealthSystem extends EntitySystem {
|
|
||||||
constructor() {
|
|
||||||
super(Matcher.all(HealthComponent));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected processEntity(entity: Entity, dt: number): void {
|
|
||||||
const health = entity.getComponent(HealthComponent)!;
|
|
||||||
// Your logic here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
- Game servers
|
|
||||||
- CLI tools with complex logic
|
|
||||||
- Simulations
|
|
||||||
- Automated testing
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [ESEngine ECS Framework](https://github.com/esengine/esengine)
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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']
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
# @esengine/demos
|
# @esengine/demos
|
||||||
|
|
||||||
|
## 1.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/fsm@1.0.3
|
||||||
|
- @esengine/pathfinding@1.0.3
|
||||||
|
- @esengine/procgen@1.0.3
|
||||||
|
- @esengine/spatial@1.0.4
|
||||||
|
- @esengine/timer@1.0.3
|
||||||
|
|
||||||
|
## 1.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/fsm@1.0.2
|
||||||
|
- @esengine/pathfinding@1.0.2
|
||||||
|
- @esengine/procgen@1.0.2
|
||||||
|
- @esengine/spatial@1.0.3
|
||||||
|
- @esengine/timer@1.0.2
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/demos",
|
"name": "@esengine/demos",
|
||||||
"version": "1.0.1",
|
"version": "1.0.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Demo tests for ESEngine modules documentation",
|
"description": "Demo tests for ESEngine modules documentation",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
88
pnpm-lock.yaml
generated
88
pnpm-lock.yaml
generated
@@ -185,7 +185,7 @@ importers:
|
|||||||
version: link:../../engine/ecs-engine-bindgen
|
version: link:../../engine/ecs-engine-bindgen
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../editor-core
|
version: link:../editor-core
|
||||||
@@ -385,7 +385,7 @@ importers:
|
|||||||
version: link:../plugins/asset-system-editor
|
version: link:../plugins/asset-system-editor
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../engine/engine-core
|
version: link:../../engine/engine-core
|
||||||
@@ -476,7 +476,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/math
|
version: link:../../framework/math
|
||||||
@@ -550,7 +550,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -596,7 +596,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -639,7 +639,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -676,7 +676,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -713,7 +713,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -756,7 +756,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -833,7 +833,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -876,7 +876,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -913,7 +913,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -953,7 +953,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -993,7 +993,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -1036,7 +1036,7 @@ importers:
|
|||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/core
|
version: link:../../../framework/core/dist
|
||||||
'@esengine/editor-core':
|
'@esengine/editor-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../editor-core
|
version: link:../../editor-core
|
||||||
@@ -1079,7 +1079,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../engine-core
|
version: link:../engine-core
|
||||||
@@ -1100,7 +1100,7 @@ importers:
|
|||||||
version: link:../asset-system
|
version: link:../asset-system
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/math
|
version: link:../../framework/math
|
||||||
@@ -1135,7 +1135,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/math
|
version: link:../../framework/math
|
||||||
@@ -1166,7 +1166,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../engine-core
|
version: link:../engine-core
|
||||||
@@ -1215,7 +1215,7 @@ importers:
|
|||||||
version: link:../asset-system
|
version: link:../asset-system
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../engine-core
|
version: link:../engine-core
|
||||||
@@ -1251,7 +1251,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/platform-common':
|
'@esengine/platform-common':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../platform-common
|
version: link:../platform-common
|
||||||
@@ -1291,7 +1291,7 @@ importers:
|
|||||||
version: link:../ecs-engine-bindgen
|
version: link:../ecs-engine-bindgen
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/math
|
version: link:../../framework/math
|
||||||
@@ -1329,7 +1329,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../engine-core
|
version: link:../engine-core
|
||||||
@@ -1357,7 +1357,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core/dist
|
||||||
'@types/jest':
|
'@types/jest':
|
||||||
specifier: ^29.5.14
|
specifier: ^29.5.14
|
||||||
version: 29.5.14
|
version: 29.5.14
|
||||||
@@ -1391,7 +1391,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core/dist
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.19.17
|
specifier: ^20.19.17
|
||||||
version: 20.19.27
|
version: 20.19.27
|
||||||
@@ -1474,6 +1474,7 @@ importers:
|
|||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: ^8.46.1
|
specifier: ^8.46.1
|
||||||
version: 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
version: 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
|
publishDirectory: dist
|
||||||
|
|
||||||
packages/framework/fsm:
|
packages/framework/fsm:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1489,7 +1490,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core/dist
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.19.17
|
specifier: ^20.19.17
|
||||||
version: 20.19.27
|
version: 20.19.27
|
||||||
@@ -1559,7 +1560,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core/dist
|
||||||
rimraf:
|
rimraf:
|
||||||
specifier: ^5.0.5
|
specifier: ^5.0.5
|
||||||
version: 5.0.10
|
version: 5.0.10
|
||||||
@@ -1599,7 +1600,7 @@ importers:
|
|||||||
version: link:../blueprint
|
version: link:../blueprint
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../math
|
version: link:../math
|
||||||
@@ -1624,7 +1625,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core/dist
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.19.17
|
specifier: ^20.19.17
|
||||||
version: 20.19.27
|
version: 20.19.27
|
||||||
@@ -1652,7 +1653,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../math
|
version: link:../math
|
||||||
@@ -1683,7 +1684,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core/dist
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.19.17
|
specifier: ^20.19.17
|
||||||
version: 20.19.27
|
version: 20.19.27
|
||||||
@@ -1705,6 +1706,9 @@ importers:
|
|||||||
tsrpc:
|
tsrpc:
|
||||||
specifier: ^3.4.15
|
specifier: ^3.4.15
|
||||||
version: 3.4.21
|
version: 3.4.21
|
||||||
|
ws:
|
||||||
|
specifier: ^8.18.0
|
||||||
|
version: 8.18.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
tsup:
|
tsup:
|
||||||
specifier: ^8.5.1
|
specifier: ^8.5.1
|
||||||
@@ -1727,7 +1731,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/math
|
version: link:../../framework/math
|
||||||
@@ -1763,7 +1767,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../engine/engine-core
|
version: link:../../engine/engine-core
|
||||||
@@ -1784,7 +1788,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/math
|
version: link:../../framework/math
|
||||||
@@ -1815,7 +1819,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.19.17
|
specifier: ^20.19.17
|
||||||
version: 20.19.27
|
version: 20.19.27
|
||||||
@@ -1846,7 +1850,7 @@ importers:
|
|||||||
version: link:../../engine/ecs-engine-bindgen
|
version: link:../../engine/ecs-engine-bindgen
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../engine/engine-core
|
version: link:../../engine/engine-core
|
||||||
@@ -1876,7 +1880,7 @@ importers:
|
|||||||
version: link:../../engine/ecs-engine-bindgen
|
version: link:../../engine/ecs-engine-bindgen
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../engine/engine-core
|
version: link:../../engine/engine-core
|
||||||
@@ -1907,7 +1911,7 @@ importers:
|
|||||||
version: link:../../engine/ecs-engine-bindgen
|
version: link:../../engine/ecs-engine-bindgen
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/math
|
version: link:../../framework/math
|
||||||
@@ -1937,7 +1941,7 @@ importers:
|
|||||||
version: link:../../tools/build-config
|
version: link:../../tools/build-config
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../engine/engine-core
|
version: link:../../engine/engine-core
|
||||||
@@ -1971,7 +1975,7 @@ importers:
|
|||||||
version: link:../../engine/ecs-engine-bindgen
|
version: link:../../engine/ecs-engine-bindgen
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/math
|
version: link:../../framework/math
|
||||||
@@ -2001,7 +2005,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../engine/engine-core
|
version: link:../../engine/engine-core
|
||||||
@@ -2097,7 +2101,7 @@ importers:
|
|||||||
version: link:../../rendering/camera
|
version: link:../../rendering/camera
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/core
|
version: link:../../framework/core/dist
|
||||||
'@esengine/ecs-framework-math':
|
'@esengine/ecs-framework-math':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../framework/math
|
version: link:../../framework/math
|
||||||
|
|||||||
Reference in New Issue
Block a user