feat(network): 基于 TSRPC 的网络同步模块 (#318)
- network-protocols: 共享协议包,使用 TSRPC CLI 生成完整类型验证 - network: 浏览器客户端,提供 NetworkPlugin、NetworkService 和同步系统 - network-server: Node.js 服务端,提供 GameServer 和房间管理
This commit is contained in:
43
packages/network-protocols/package.json
Normal file
43
packages/network-protocols/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@esengine/network-protocols",
|
||||
"version": "1.0.0",
|
||||
"description": "Shared network protocols for client and server",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"proto": "tsrpc proto --input src/shared/protocols --output src/shared/protocols/serviceProto.ts",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsrpc-cli": "^2.4.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"network",
|
||||
"protocols",
|
||||
"tsrpc"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tsrpc-proto": "^1.4.3"
|
||||
}
|
||||
}
|
||||
34
packages/network-protocols/src/index.ts
Normal file
34
packages/network-protocols/src/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @esengine/network-protocols
|
||||
*
|
||||
* 基于 TSRPC 的共享网络协议
|
||||
* TSRPC-based shared network protocols
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Service Protocol | 服务协议
|
||||
// ============================================================================
|
||||
|
||||
export { serviceProto } from './shared/protocols/serviceProto';
|
||||
export type { ServiceType } from './shared/protocols/serviceProto';
|
||||
|
||||
// ============================================================================
|
||||
// Types | 类型定义
|
||||
// ============================================================================
|
||||
|
||||
export type { Vec2, IEntityState, IPlayerInput } from './shared/protocols/types';
|
||||
|
||||
// ============================================================================
|
||||
// API Protocols | API 协议
|
||||
// ============================================================================
|
||||
|
||||
export type { ReqJoin, ResJoin } from './shared/protocols/PtlJoin';
|
||||
|
||||
// ============================================================================
|
||||
// Message Protocols | 消息协议
|
||||
// ============================================================================
|
||||
|
||||
export type { MsgSync } from './shared/protocols/MsgSync';
|
||||
export type { MsgInput } from './shared/protocols/MsgInput';
|
||||
export type { MsgSpawn } from './shared/protocols/MsgSpawn';
|
||||
export type { MsgDespawn } from './shared/protocols/MsgDespawn';
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 销毁实体消息
|
||||
* Despawn entity message
|
||||
*/
|
||||
|
||||
export interface MsgDespawn {
|
||||
/** 网络 ID | Network ID */
|
||||
netId: number;
|
||||
}
|
||||
11
packages/network-protocols/src/shared/protocols/MsgInput.ts
Normal file
11
packages/network-protocols/src/shared/protocols/MsgInput.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 输入消息
|
||||
* Input message
|
||||
*/
|
||||
|
||||
import type { IPlayerInput } from './types';
|
||||
|
||||
export interface MsgInput {
|
||||
/** 玩家输入 | Player input */
|
||||
input: IPlayerInput;
|
||||
}
|
||||
19
packages/network-protocols/src/shared/protocols/MsgSpawn.ts
Normal file
19
packages/network-protocols/src/shared/protocols/MsgSpawn.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 生成实体消息
|
||||
* Spawn entity message
|
||||
*/
|
||||
|
||||
import type { Vec2 } from './types';
|
||||
|
||||
export interface MsgSpawn {
|
||||
/** 网络 ID | Network ID */
|
||||
netId: number;
|
||||
/** 所有者客户端 ID | Owner client ID */
|
||||
ownerId: number;
|
||||
/** 预制体类型 | Prefab type */
|
||||
prefab: string;
|
||||
/** 初始位置 | Initial position */
|
||||
pos: Vec2;
|
||||
/** 初始旋转 | Initial rotation */
|
||||
rot: number;
|
||||
}
|
||||
13
packages/network-protocols/src/shared/protocols/MsgSync.ts
Normal file
13
packages/network-protocols/src/shared/protocols/MsgSync.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 状态同步消息
|
||||
* State sync message
|
||||
*/
|
||||
|
||||
import type { IEntityState } from './types';
|
||||
|
||||
export interface MsgSync {
|
||||
/** 服务器时间戳 | Server timestamp */
|
||||
time: number;
|
||||
/** 实体状态列表 | Entity state list */
|
||||
entities: IEntityState[];
|
||||
}
|
||||
28
packages/network-protocols/src/shared/protocols/PtlJoin.ts
Normal file
28
packages/network-protocols/src/shared/protocols/PtlJoin.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 加入房间 API
|
||||
* Join room API
|
||||
*/
|
||||
|
||||
/**
|
||||
* 加入请求
|
||||
* Join request
|
||||
*/
|
||||
export interface ReqJoin {
|
||||
/** 玩家名称 | Player name */
|
||||
playerName: string;
|
||||
/** 房间 ID(可选,不传则自动匹配)| Room ID (optional, auto-match if not provided) */
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入响应
|
||||
* Join response
|
||||
*/
|
||||
export interface ResJoin {
|
||||
/** 分配的客户端 ID | Assigned client ID */
|
||||
clientId: number;
|
||||
/** 房间 ID | Room ID */
|
||||
roomId: string;
|
||||
/** 房间当前玩家数 | Current player count in room */
|
||||
playerCount: number;
|
||||
}
|
||||
268
packages/network-protocols/src/shared/protocols/serviceProto.ts
Normal file
268
packages/network-protocols/src/shared/protocols/serviceProto.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { ServiceProto } from 'tsrpc-proto';
|
||||
import { MsgDespawn } from './MsgDespawn';
|
||||
import { MsgInput } from './MsgInput';
|
||||
import { MsgSpawn } from './MsgSpawn';
|
||||
import { MsgSync } from './MsgSync';
|
||||
import { ReqJoin, ResJoin } from './PtlJoin';
|
||||
|
||||
export interface ServiceType {
|
||||
api: {
|
||||
"Join": {
|
||||
req: ReqJoin,
|
||||
res: ResJoin
|
||||
}
|
||||
},
|
||||
msg: {
|
||||
"Despawn": MsgDespawn,
|
||||
"Input": MsgInput,
|
||||
"Spawn": MsgSpawn,
|
||||
"Sync": MsgSync
|
||||
}
|
||||
}
|
||||
|
||||
export const serviceProto: ServiceProto<ServiceType> = {
|
||||
"services": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Despawn",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Input",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Spawn",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Sync",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Join",
|
||||
"type": "api"
|
||||
}
|
||||
],
|
||||
"types": {
|
||||
"MsgDespawn/MsgDespawn": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "netId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"MsgInput/MsgInput": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "input",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "types/IPlayerInput"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"types/IPlayerInput": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "frame",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "moveDir",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "types/Vec2"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "actions",
|
||||
"type": {
|
||||
"type": "Array",
|
||||
"elementType": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"types/Vec2": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "x",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "y",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"MsgSpawn/MsgSpawn": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "netId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ownerId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "prefab",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "pos",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "types/Vec2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "rot",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"MsgSync/MsgSync": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "time",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "entities",
|
||||
"type": {
|
||||
"type": "Array",
|
||||
"elementType": {
|
||||
"type": "Reference",
|
||||
"target": "types/IEntityState"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"types/IEntityState": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "netId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "pos",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "types/Vec2"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "rot",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlJoin/ReqJoin": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "playerName",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "roomId",
|
||||
"type": {
|
||||
"type": "String"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlJoin/ResJoin": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "clientId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "roomId",
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "playerCount",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
39
packages/network-protocols/src/shared/protocols/types.ts
Normal file
39
packages/network-protocols/src/shared/protocols/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 共享类型定义
|
||||
* Shared type definitions
|
||||
*/
|
||||
|
||||
/**
|
||||
* 二维向量
|
||||
* 2D Vector
|
||||
*/
|
||||
export interface Vec2 {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体状态
|
||||
* Entity state
|
||||
*/
|
||||
export interface IEntityState {
|
||||
/** 网络 ID | Network ID */
|
||||
netId: number;
|
||||
/** 位置 | Position */
|
||||
pos?: Vec2;
|
||||
/** 旋转角度(弧度)| Rotation (radians) */
|
||||
rot?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家输入
|
||||
* Player input
|
||||
*/
|
||||
export interface IPlayerInput {
|
||||
/** 帧号 | Frame number */
|
||||
frame: number;
|
||||
/** 移动方向 | Move direction */
|
||||
moveDir?: Vec2;
|
||||
/** 动作列表 | Action list */
|
||||
actions?: string[];
|
||||
}
|
||||
13
packages/network-protocols/tsconfig.build.json
Normal file
13
packages/network-protocols/tsconfig.build.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
11
packages/network-protocols/tsconfig.json
Normal file
11
packages/network-protocols/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
11
packages/network-protocols/tsup.config.ts
Normal file
11
packages/network-protocols/tsup.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
dts: true,
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
40
packages/network-server/package.json
Normal file
40
packages/network-server/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@esengine/network-server",
|
||||
"version": "1.0.0",
|
||||
"description": "TSRPC-based network server for ESEngine",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsx src/main.ts",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/network-protocols": "workspace:*",
|
||||
"tsrpc": "^3.4.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^8.5.1",
|
||||
"tsx": "^4.19.0"
|
||||
},
|
||||
"keywords": [
|
||||
"esengine",
|
||||
"network",
|
||||
"server",
|
||||
"tsrpc",
|
||||
"websocket"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
31
packages/network-server/src/index.ts
Normal file
31
packages/network-server/src/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @esengine/network-server
|
||||
*
|
||||
* 基于 TSRPC 的网络服务器模块
|
||||
* TSRPC-based network server module
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Re-export from protocols | 从协议包重新导出
|
||||
// ============================================================================
|
||||
|
||||
export type {
|
||||
ServiceType,
|
||||
IEntityState,
|
||||
IPlayerInput,
|
||||
MsgSync,
|
||||
MsgInput,
|
||||
MsgSpawn,
|
||||
MsgDespawn,
|
||||
ReqJoin,
|
||||
ResJoin
|
||||
} from '@esengine/network-protocols';
|
||||
|
||||
export { serviceProto } from '@esengine/network-protocols';
|
||||
|
||||
// ============================================================================
|
||||
// Server | 服务器
|
||||
// ============================================================================
|
||||
|
||||
export { GameServer, type IServerConfig } from './services/GameServer';
|
||||
export { Room, type IPlayer, type IRoomConfig } from './services/Room';
|
||||
35
packages/network-server/src/main.ts
Normal file
35
packages/network-server/src/main.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 服务器入口
|
||||
* Server entry point
|
||||
*/
|
||||
import { GameServer } from './services/GameServer';
|
||||
|
||||
const PORT = parseInt(process.env['PORT'] ?? '3000', 10);
|
||||
|
||||
const server = new GameServer({
|
||||
port: PORT,
|
||||
roomConfig: {
|
||||
maxPlayers: 16,
|
||||
tickRate: 20
|
||||
}
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
// Start server
|
||||
server.start().catch((err) => {
|
||||
console.error('[Main] 服务器启动失败 | Server failed to start:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n[Main] 正在关闭服务器... | Shutting down server...');
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
197
packages/network-server/src/services/GameServer.ts
Normal file
197
packages/network-server/src/services/GameServer.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { WsServer, type BaseConnection } from 'tsrpc';
|
||||
import { serviceProto, type ServiceType, type MsgInput } from '@esengine/network-protocols';
|
||||
import { Room, type IRoomConfig } from './Room';
|
||||
|
||||
/**
|
||||
* 服务器配置
|
||||
* Server configuration
|
||||
*/
|
||||
export interface IServerConfig {
|
||||
port: number;
|
||||
roomConfig?: Partial<IRoomConfig>;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: IServerConfig = {
|
||||
port: 3000
|
||||
};
|
||||
|
||||
/**
|
||||
* 游戏服务器
|
||||
* Game server
|
||||
*
|
||||
* 管理 WebSocket 连接和房间。
|
||||
* Manages WebSocket connections and rooms.
|
||||
*/
|
||||
export class GameServer {
|
||||
private _server: WsServer<ServiceType>;
|
||||
private _config: IServerConfig;
|
||||
private _rooms: Map<string, Room> = new Map();
|
||||
private _connectionToRoom: Map<BaseConnection<ServiceType>, { roomId: string; clientId: number }> = new Map();
|
||||
|
||||
constructor(config: Partial<IServerConfig> = {}) {
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
this._server = new WsServer(serviceProto, {
|
||||
port: this._config.port,
|
||||
json: true,
|
||||
logLevel: 'info'
|
||||
});
|
||||
|
||||
this._setupHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务器
|
||||
* Start server
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
await this._server.start();
|
||||
console.log(`[GameServer] 服务器已启动 | Server started on port ${this._config.port}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止服务器
|
||||
* Stop server
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
// 销毁所有房间
|
||||
// Destroy all rooms
|
||||
for (const room of this._rooms.values()) {
|
||||
room.destroy();
|
||||
}
|
||||
this._rooms.clear();
|
||||
this._connectionToRoom.clear();
|
||||
|
||||
await this._server.stop();
|
||||
console.log('[GameServer] 服务器已停止 | Server stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建房间
|
||||
* Get or create room
|
||||
*/
|
||||
getOrCreateRoom(roomId?: string): Room {
|
||||
// 如果没有指定房间 ID,寻找未满的房间或创建新房间
|
||||
// If no room ID specified, find a non-full room or create new one
|
||||
if (!roomId) {
|
||||
for (const room of this._rooms.values()) {
|
||||
if (!room.isFull) {
|
||||
return room;
|
||||
}
|
||||
}
|
||||
roomId = this._generateRoomId();
|
||||
}
|
||||
|
||||
let room = this._rooms.get(roomId);
|
||||
if (!room) {
|
||||
room = new Room(roomId, this._config.roomConfig);
|
||||
this._rooms.set(roomId, room);
|
||||
console.log(`[GameServer] 创建房间 | Room created: ${roomId}`);
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间
|
||||
* Get room
|
||||
*/
|
||||
getRoom(roomId: string): Room | undefined {
|
||||
return this._rooms.get(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接的房间信息
|
||||
* Get connection's room info
|
||||
*/
|
||||
getConnectionInfo(connection: BaseConnection<ServiceType>): { roomId: string; clientId: number } | undefined {
|
||||
return this._connectionToRoom.get(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接的房间信息
|
||||
* Set connection's room info
|
||||
*/
|
||||
setConnectionInfo(connection: BaseConnection<ServiceType>, roomId: string, clientId: number): void {
|
||||
this._connectionToRoom.set(connection, { roomId, clientId });
|
||||
}
|
||||
|
||||
private _setupHandlers(): void {
|
||||
// 处理加入请求
|
||||
// Handle join request
|
||||
this._server.implementApi('Join', async (call) => {
|
||||
const { playerName, roomId } = call.req;
|
||||
|
||||
const room = this.getOrCreateRoom(roomId);
|
||||
if (room.isFull) {
|
||||
call.error('房间已满 | Room is full');
|
||||
return;
|
||||
}
|
||||
|
||||
const player = room.addPlayer(playerName, call.conn);
|
||||
if (!player) {
|
||||
call.error('加入房间失败 | Failed to join room');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setConnectionInfo(call.conn, room.id, player.clientId);
|
||||
|
||||
// 向新玩家发送自己的生成消息
|
||||
// Send spawn message to new player for themselves
|
||||
call.conn.sendMsg('Spawn', {
|
||||
netId: player.netId,
|
||||
ownerId: player.clientId,
|
||||
prefab: 'player',
|
||||
pos: { x: 0, y: 0 },
|
||||
rot: 0
|
||||
});
|
||||
|
||||
call.succ({
|
||||
clientId: player.clientId,
|
||||
roomId: room.id,
|
||||
playerCount: room.playerCount
|
||||
});
|
||||
|
||||
console.log(`[GameServer] 玩家加入 | Player joined: ${playerName} (${player.clientId}) -> ${room.id}`);
|
||||
});
|
||||
|
||||
// 处理输入消息
|
||||
// Handle input message
|
||||
this._server.listenMsg('Input', (call) => {
|
||||
const info = this.getConnectionInfo(call.conn);
|
||||
if (!info) return;
|
||||
|
||||
const room = this.getRoom(info.roomId);
|
||||
if (!room) return;
|
||||
|
||||
const msg = call.msg as MsgInput;
|
||||
room.handleInput(info.clientId, msg.input);
|
||||
});
|
||||
|
||||
// 处理断开连接
|
||||
// Handle disconnect
|
||||
this._server.flows.postDisconnectFlow.push((v) => {
|
||||
const info = this._connectionToRoom.get(v.conn);
|
||||
if (info) {
|
||||
const room = this.getRoom(info.roomId);
|
||||
if (room) {
|
||||
room.removePlayer(info.clientId);
|
||||
console.log(`[GameServer] 玩家离开 | Player left: ${info.clientId} from ${info.roomId}`);
|
||||
|
||||
// 如果房间空了,删除房间
|
||||
// If room is empty, delete it
|
||||
if (room.playerCount === 0) {
|
||||
room.destroy();
|
||||
this._rooms.delete(info.roomId);
|
||||
console.log(`[GameServer] 删除空房间 | Empty room deleted: ${info.roomId}`);
|
||||
}
|
||||
}
|
||||
this._connectionToRoom.delete(v.conn);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
private _generateRoomId(): string {
|
||||
return Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
}
|
||||
}
|
||||
234
packages/network-server/src/services/Room.ts
Normal file
234
packages/network-server/src/services/Room.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import type { BaseConnection } from 'tsrpc';
|
||||
import type { ServiceType, IEntityState } from '@esengine/network-protocols';
|
||||
|
||||
/**
|
||||
* 连接类型别名
|
||||
* Connection type alias
|
||||
*/
|
||||
type Connection = BaseConnection<ServiceType>;
|
||||
|
||||
/**
|
||||
* 玩家信息
|
||||
* Player information
|
||||
*/
|
||||
export interface IPlayer {
|
||||
clientId: number;
|
||||
name: string;
|
||||
connection: Connection;
|
||||
netId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间配置
|
||||
* Room configuration
|
||||
*/
|
||||
export interface IRoomConfig {
|
||||
maxPlayers: number;
|
||||
tickRate: number;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: IRoomConfig = {
|
||||
maxPlayers: 16,
|
||||
tickRate: 20
|
||||
};
|
||||
|
||||
/**
|
||||
* 游戏房间
|
||||
* Game room
|
||||
*
|
||||
* 管理房间内的玩家和实体状态同步。
|
||||
* Manages players and entity state synchronization within a room.
|
||||
*/
|
||||
export class Room {
|
||||
private _id: string;
|
||||
private _config: IRoomConfig;
|
||||
private _players: Map<number, IPlayer> = new Map();
|
||||
private _entities: Map<number, IEntityState> = new Map();
|
||||
private _nextClientId: number = 1;
|
||||
private _nextNetId: number = 1;
|
||||
private _syncInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor(id: string, config: Partial<IRoomConfig> = {}) {
|
||||
this._id = id;
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get playerCount(): number {
|
||||
return this._players.size;
|
||||
}
|
||||
|
||||
get isFull(): boolean {
|
||||
return this._players.size >= this._config.maxPlayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加玩家
|
||||
* Add player
|
||||
*/
|
||||
addPlayer(name: string, connection: Connection): IPlayer | null {
|
||||
if (this.isFull) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clientId = this._nextClientId++;
|
||||
const netId = this._nextNetId++;
|
||||
|
||||
const player: IPlayer = {
|
||||
clientId,
|
||||
name,
|
||||
connection,
|
||||
netId
|
||||
};
|
||||
|
||||
this._players.set(clientId, player);
|
||||
|
||||
// 创建玩家实体
|
||||
// Create player entity
|
||||
const entityState: IEntityState = {
|
||||
netId,
|
||||
pos: { x: 0, y: 0 },
|
||||
rot: 0
|
||||
};
|
||||
this._entities.set(netId, entityState);
|
||||
|
||||
// 通知其他玩家
|
||||
// Notify other players
|
||||
this._broadcastSpawn(player, entityState);
|
||||
|
||||
// 同步现有实体给新玩家
|
||||
// Sync existing entities to new player
|
||||
this._syncExistingEntities(player);
|
||||
|
||||
// 启动同步循环
|
||||
// Start sync loop
|
||||
if (this._syncInterval === null) {
|
||||
this._startSyncLoop();
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除玩家
|
||||
* Remove player
|
||||
*/
|
||||
removePlayer(clientId: number): void {
|
||||
const player = this._players.get(clientId);
|
||||
if (!player) return;
|
||||
|
||||
this._players.delete(clientId);
|
||||
this._entities.delete(player.netId);
|
||||
|
||||
// 通知其他玩家
|
||||
// Notify other players
|
||||
this._broadcastDespawn(player.netId);
|
||||
|
||||
// 停止同步循环
|
||||
// Stop sync loop
|
||||
if (this._players.size === 0 && this._syncInterval !== null) {
|
||||
clearInterval(this._syncInterval);
|
||||
this._syncInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理玩家输入
|
||||
* Handle player input
|
||||
*/
|
||||
handleInput(
|
||||
clientId: number,
|
||||
input: { moveDir?: { x: number; y: number }; actions?: string[] }
|
||||
): void {
|
||||
const player = this._players.get(clientId);
|
||||
if (!player) return;
|
||||
|
||||
const entity = this._entities.get(player.netId);
|
||||
if (!entity || !entity.pos) return;
|
||||
|
||||
// 简单的移动处理
|
||||
// Simple movement handling
|
||||
if (input.moveDir) {
|
||||
const speed = 5;
|
||||
entity.pos.x += input.moveDir.x * speed;
|
||||
entity.pos.y += input.moveDir.y * speed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家
|
||||
* Get player
|
||||
*/
|
||||
getPlayer(clientId: number): IPlayer | undefined {
|
||||
return this._players.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁房间
|
||||
* Destroy room
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this._syncInterval !== null) {
|
||||
clearInterval(this._syncInterval);
|
||||
this._syncInterval = null;
|
||||
}
|
||||
this._players.clear();
|
||||
this._entities.clear();
|
||||
}
|
||||
|
||||
private _startSyncLoop(): void {
|
||||
const interval = 1000 / this._config.tickRate;
|
||||
this._syncInterval = setInterval(() => {
|
||||
this._broadcastSync();
|
||||
}, interval);
|
||||
}
|
||||
|
||||
private _broadcastSync(): void {
|
||||
if (this._players.size === 0) return;
|
||||
|
||||
const entities = Array.from(this._entities.values());
|
||||
const time = Date.now();
|
||||
|
||||
for (const player of this._players.values()) {
|
||||
player.connection.sendMsg('Sync', { time, entities });
|
||||
}
|
||||
}
|
||||
|
||||
private _broadcastSpawn(newPlayer: IPlayer, state: IEntityState): void {
|
||||
for (const player of this._players.values()) {
|
||||
if (player.clientId === newPlayer.clientId) continue;
|
||||
|
||||
player.connection.sendMsg('Spawn', {
|
||||
netId: state.netId,
|
||||
ownerId: newPlayer.clientId,
|
||||
prefab: 'player',
|
||||
pos: state.pos ?? { x: 0, y: 0 },
|
||||
rot: state.rot ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _broadcastDespawn(netId: number): void {
|
||||
for (const player of this._players.values()) {
|
||||
player.connection.sendMsg('Despawn', { netId });
|
||||
}
|
||||
}
|
||||
|
||||
private _syncExistingEntities(newPlayer: IPlayer): void {
|
||||
for (const [netId, state] of this._entities) {
|
||||
const owner = Array.from(this._players.values()).find((p) => p.netId === netId);
|
||||
if (!owner || owner.clientId === newPlayer.clientId) continue;
|
||||
|
||||
newPlayer.connection.sendMsg('Spawn', {
|
||||
netId,
|
||||
ownerId: owner.clientId,
|
||||
prefab: 'player',
|
||||
pos: state.pos ?? { x: 0, y: 0 },
|
||||
rot: state.rot ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
13
packages/network-server/tsconfig.build.json
Normal file
13
packages/network-server/tsconfig.build.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
14
packages/network-server/tsconfig.json
Normal file
14
packages/network-server/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{ "path": "../network-protocols" }
|
||||
]
|
||||
}
|
||||
11
packages/network-server/tsup.config.ts
Normal file
11
packages/network-server/tsup.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts', 'src/main.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
external: ['tsrpc'],
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
34
packages/network/module.json
Normal file
34
packages/network/module.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@esengine/network",
|
||||
"displayName": "Network",
|
||||
"description": "TSRPC-based network synchronization for multiplayer games",
|
||||
"version": "1.0.0",
|
||||
"category": "network",
|
||||
"dependencies": [],
|
||||
"components": [
|
||||
{
|
||||
"name": "NetworkIdentity",
|
||||
"displayName": "Network Identity",
|
||||
"description": "Identifies an entity on the network"
|
||||
},
|
||||
{
|
||||
"name": "NetworkTransform",
|
||||
"displayName": "Network Transform",
|
||||
"description": "Syncs entity position and rotation"
|
||||
}
|
||||
],
|
||||
"systems": [
|
||||
{
|
||||
"name": "NetworkSyncSystem",
|
||||
"description": "Handles state synchronization"
|
||||
},
|
||||
{
|
||||
"name": "NetworkSpawnSystem",
|
||||
"description": "Handles entity spawning/despawning"
|
||||
},
|
||||
{
|
||||
"name": "NetworkInputSystem",
|
||||
"description": "Handles input collection and sending"
|
||||
}
|
||||
]
|
||||
}
|
||||
50
packages/network/package.json
Normal file
50
packages/network/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@esengine/network",
|
||||
"version": "1.0.0",
|
||||
"description": "Network synchronization for multiplayer games based on TSRPC",
|
||||
"esengine": {
|
||||
"plugin": true,
|
||||
"pluginExport": "NetworkPlugin",
|
||||
"category": "network",
|
||||
"isEnginePlugin": true
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup && tsc --project tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/network-protocols": "workspace:*",
|
||||
"tsrpc-browser": "^3.4.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"network",
|
||||
"multiplayer",
|
||||
"websocket",
|
||||
"sync"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT"
|
||||
}
|
||||
153
packages/network/src/NetworkPlugin.ts
Normal file
153
packages/network/src/NetworkPlugin.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { type IPlugin, Core, type ServiceContainer, type Scene } from '@esengine/ecs-framework';
|
||||
import { NetworkService } from './services/NetworkService';
|
||||
import { NetworkSyncSystem } from './systems/NetworkSyncSystem';
|
||||
import { NetworkSpawnSystem, type PrefabFactory } from './systems/NetworkSpawnSystem';
|
||||
import { NetworkInputSystem } from './systems/NetworkInputSystem';
|
||||
|
||||
/**
|
||||
* 网络插件
|
||||
* Network plugin
|
||||
*
|
||||
* 提供基于 TSRPC 的网络同步功能。
|
||||
* Provides TSRPC-based network synchronization.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Core } from '@esengine/ecs-framework';
|
||||
* import { NetworkPlugin } from '@esengine/network';
|
||||
*
|
||||
* const networkPlugin = new NetworkPlugin();
|
||||
* await Core.installPlugin(networkPlugin);
|
||||
*
|
||||
* // 连接到服务器 | Connect to server
|
||||
* await networkPlugin.connect('ws://localhost:3000', 'Player1');
|
||||
*
|
||||
* // 注册预制体 | Register prefab
|
||||
* networkPlugin.registerPrefab('player', (scene, spawn) => {
|
||||
* const entity = scene.createEntity('Player');
|
||||
* return entity;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class NetworkPlugin implements IPlugin {
|
||||
public readonly name = '@esengine/network';
|
||||
public readonly version = '1.0.0';
|
||||
|
||||
private _networkService!: NetworkService;
|
||||
private _syncSystem!: NetworkSyncSystem;
|
||||
private _spawnSystem!: NetworkSpawnSystem;
|
||||
private _inputSystem!: NetworkInputSystem;
|
||||
|
||||
/**
|
||||
* 网络服务
|
||||
* Network service
|
||||
*/
|
||||
get networkService(): NetworkService {
|
||||
return this._networkService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步系统
|
||||
* Sync system
|
||||
*/
|
||||
get syncSystem(): NetworkSyncSystem {
|
||||
return this._syncSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成系统
|
||||
* Spawn system
|
||||
*/
|
||||
get spawnSystem(): NetworkSpawnSystem {
|
||||
return this._spawnSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入系统
|
||||
* Input system
|
||||
*/
|
||||
get inputSystem(): NetworkInputSystem {
|
||||
return this._inputSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已连接
|
||||
* Is connected
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this._networkService?.isConnected ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
* Install plugin
|
||||
*/
|
||||
install(_core: Core, _services: ServiceContainer): void {
|
||||
this._networkService = new NetworkService();
|
||||
|
||||
// 当场景加载时添加系统
|
||||
// Add systems when scene loads
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
this._setupSystems(scene as Scene);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
* Uninstall plugin
|
||||
*/
|
||||
uninstall(): void {
|
||||
this._networkService?.disconnect();
|
||||
}
|
||||
|
||||
private _setupSystems(scene: Scene): void {
|
||||
this._syncSystem = new NetworkSyncSystem(this._networkService);
|
||||
this._spawnSystem = new NetworkSpawnSystem(this._networkService, this._syncSystem);
|
||||
this._inputSystem = new NetworkInputSystem(this._networkService);
|
||||
|
||||
scene.addSystem(this._syncSystem);
|
||||
scene.addSystem(this._spawnSystem);
|
||||
scene.addSystem(this._inputSystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
* Connect to server
|
||||
*/
|
||||
public async connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean> {
|
||||
return this._networkService.connect(serverUrl, playerName, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
* Disconnect
|
||||
*/
|
||||
public async disconnect(): Promise<void> {
|
||||
await this._networkService.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册预制体工厂
|
||||
* Register prefab factory
|
||||
*/
|
||||
public registerPrefab(prefabType: string, factory: PrefabFactory): void {
|
||||
this._spawnSystem?.registerPrefab(prefabType, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送移动输入
|
||||
* Send move input
|
||||
*/
|
||||
public sendMoveInput(x: number, y: number): void {
|
||||
this._inputSystem?.addMoveInput(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送动作输入
|
||||
* Send action input
|
||||
*/
|
||||
public sendActionInput(action: string): void {
|
||||
this._inputSystem?.addActionInput(action);
|
||||
}
|
||||
}
|
||||
70
packages/network/src/components/NetworkIdentity.ts
Normal file
70
packages/network/src/components/NetworkIdentity.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Component, ECSComponent, Serialize, Serializable, Property } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 网络身份组件
|
||||
* Network identity component
|
||||
*
|
||||
* 标识一个实体在网络上的唯一身份。
|
||||
* Identifies an entity's unique identity on the network.
|
||||
*/
|
||||
@ECSComponent('NetworkIdentity')
|
||||
@Serializable({ version: 1, typeId: 'NetworkIdentity' })
|
||||
export class NetworkIdentity extends Component {
|
||||
/**
|
||||
* 网络实体 ID
|
||||
* Network entity ID
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Net ID', readOnly: true })
|
||||
public netId: number = 0;
|
||||
|
||||
/**
|
||||
* 所有者客户端 ID
|
||||
* Owner client ID
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Owner ID', readOnly: true })
|
||||
public ownerId: number = 0;
|
||||
|
||||
/**
|
||||
* 是否为本地玩家拥有
|
||||
* Is owned by local player
|
||||
*/
|
||||
public bIsLocalPlayer: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否有权限控制
|
||||
* Has authority
|
||||
*/
|
||||
public bHasAuthority: boolean = false;
|
||||
|
||||
/**
|
||||
* 预制体类型
|
||||
* Prefab type
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'string', label: 'Prefab Type' })
|
||||
public prefabType: string = '';
|
||||
|
||||
/**
|
||||
* 同步间隔 (ms)
|
||||
* Sync interval in milliseconds
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Sync Interval', min: 16 })
|
||||
public syncInterval: number = 100;
|
||||
|
||||
/**
|
||||
* 上次同步时间
|
||||
* Last sync time
|
||||
*/
|
||||
public lastSyncTime: number = 0;
|
||||
|
||||
/**
|
||||
* 检查是否需要同步
|
||||
* Check if sync is needed
|
||||
*/
|
||||
public needsSync(now: number): boolean {
|
||||
return now - this.lastSyncTime >= this.syncInterval;
|
||||
}
|
||||
}
|
||||
100
packages/network/src/components/NetworkTransform.ts
Normal file
100
packages/network/src/components/NetworkTransform.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Component, ECSComponent, Serialize, Serializable, Property } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 网络变换组件
|
||||
* Network transform component
|
||||
*
|
||||
* 同步实体的位置和旋转。支持插值平滑。
|
||||
* Syncs entity position and rotation with interpolation smoothing.
|
||||
*/
|
||||
@ECSComponent('NetworkTransform', { requires: ['NetworkIdentity'] })
|
||||
@Serializable({ version: 1, typeId: 'NetworkTransform' })
|
||||
export class NetworkTransform extends Component {
|
||||
/**
|
||||
* 目标位置 X
|
||||
* Target position X
|
||||
*/
|
||||
public targetX: number = 0;
|
||||
|
||||
/**
|
||||
* 目标位置 Y
|
||||
* Target position Y
|
||||
*/
|
||||
public targetY: number = 0;
|
||||
|
||||
/**
|
||||
* 目标旋转
|
||||
* Target rotation
|
||||
*/
|
||||
public targetRotation: number = 0;
|
||||
|
||||
/**
|
||||
* 当前位置 X
|
||||
* Current position X
|
||||
*/
|
||||
public currentX: number = 0;
|
||||
|
||||
/**
|
||||
* 当前位置 Y
|
||||
* Current position Y
|
||||
*/
|
||||
public currentY: number = 0;
|
||||
|
||||
/**
|
||||
* 当前旋转
|
||||
* Current rotation
|
||||
*/
|
||||
public currentRotation: number = 0;
|
||||
|
||||
/**
|
||||
* 插值速度
|
||||
* Interpolation speed
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Lerp Speed', min: 0.1, max: 50 })
|
||||
public lerpSpeed: number = 10;
|
||||
|
||||
/**
|
||||
* 是否启用插值
|
||||
* Enable interpolation
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Interpolate' })
|
||||
public bInterpolate: boolean = true;
|
||||
|
||||
/**
|
||||
* 同步间隔 (ms)
|
||||
* Sync interval in milliseconds
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Sync Interval', min: 16 })
|
||||
public syncInterval: number = 50;
|
||||
|
||||
/**
|
||||
* 上次同步时间
|
||||
* Last sync time
|
||||
*/
|
||||
public lastSyncTime: number = 0;
|
||||
|
||||
/**
|
||||
* 设置目标位置
|
||||
* Set target position
|
||||
*/
|
||||
public setTarget(x: number, y: number, rotation?: number): void {
|
||||
this.targetX = x;
|
||||
this.targetY = y;
|
||||
if (rotation !== undefined) {
|
||||
this.targetRotation = rotation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即跳转到目标位置
|
||||
* Snap to target position immediately
|
||||
*/
|
||||
public snap(): void {
|
||||
this.currentX = this.targetX;
|
||||
this.currentY = this.targetY;
|
||||
this.currentRotation = this.targetRotation;
|
||||
}
|
||||
}
|
||||
65
packages/network/src/index.ts
Normal file
65
packages/network/src/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @esengine/network
|
||||
*
|
||||
* 基于 TSRPC 的网络同步模块(客户端)
|
||||
* TSRPC-based network synchronization module (client)
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Re-export from protocols | 从协议包重新导出
|
||||
// ============================================================================
|
||||
|
||||
export type {
|
||||
ServiceType,
|
||||
Vec2,
|
||||
IEntityState,
|
||||
IPlayerInput,
|
||||
MsgSync,
|
||||
MsgInput,
|
||||
MsgSpawn,
|
||||
MsgDespawn,
|
||||
ReqJoin,
|
||||
ResJoin
|
||||
} from '@esengine/network-protocols';
|
||||
|
||||
export { serviceProto } from '@esengine/network-protocols';
|
||||
|
||||
// ============================================================================
|
||||
// Tokens | 服务令牌
|
||||
// ============================================================================
|
||||
|
||||
export {
|
||||
NetworkServiceToken,
|
||||
NetworkSyncSystemToken,
|
||||
NetworkSpawnSystemToken,
|
||||
NetworkInputSystemToken
|
||||
} from './tokens';
|
||||
|
||||
// ============================================================================
|
||||
// Plugin | 插件
|
||||
// ============================================================================
|
||||
|
||||
export { NetworkPlugin } from './NetworkPlugin';
|
||||
|
||||
// ============================================================================
|
||||
// Services | 服务
|
||||
// ============================================================================
|
||||
|
||||
export { NetworkService, ENetworkState } from './services/NetworkService';
|
||||
export type { INetworkCallbacks } from './services/NetworkService';
|
||||
|
||||
// ============================================================================
|
||||
// Components | 组件
|
||||
// ============================================================================
|
||||
|
||||
export { NetworkIdentity } from './components/NetworkIdentity';
|
||||
export { NetworkTransform } from './components/NetworkTransform';
|
||||
|
||||
// ============================================================================
|
||||
// Systems | 系统
|
||||
// ============================================================================
|
||||
|
||||
export { NetworkSyncSystem } from './systems/NetworkSyncSystem';
|
||||
export { NetworkSpawnSystem } from './systems/NetworkSpawnSystem';
|
||||
export type { PrefabFactory } from './systems/NetworkSpawnSystem';
|
||||
export { NetworkInputSystem } from './systems/NetworkInputSystem';
|
||||
172
packages/network/src/services/NetworkService.ts
Normal file
172
packages/network/src/services/NetworkService.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { WsClient } from 'tsrpc-browser';
|
||||
import {
|
||||
serviceProto,
|
||||
type ServiceType,
|
||||
type MsgSync,
|
||||
type MsgSpawn,
|
||||
type MsgDespawn,
|
||||
type IPlayerInput
|
||||
} from '@esengine/network-protocols';
|
||||
|
||||
/**
|
||||
* 连接状态
|
||||
* Connection state
|
||||
*/
|
||||
export const enum ENetworkState {
|
||||
Disconnected = 0,
|
||||
Connecting = 1,
|
||||
Connected = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络事件回调
|
||||
* Network event callbacks
|
||||
*/
|
||||
export interface INetworkCallbacks {
|
||||
onConnected?: (clientId: number, roomId: string) => void;
|
||||
onDisconnected?: () => void;
|
||||
onSync?: (msg: MsgSync) => void;
|
||||
onSpawn?: (msg: MsgSpawn) => void;
|
||||
onDespawn?: (msg: MsgDespawn) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 TSRPC 客户端
|
||||
* Create TSRPC client
|
||||
*/
|
||||
function createClient(serverUrl: string): WsClient<ServiceType> {
|
||||
return new WsClient(serviceProto, {
|
||||
server: serverUrl,
|
||||
json: true,
|
||||
logLevel: 'warn'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络服务
|
||||
* Network service
|
||||
*
|
||||
* 基于 TSRPC 的网络服务封装,提供类型安全的网络通信。
|
||||
* TSRPC-based network service wrapper with type-safe communication.
|
||||
*/
|
||||
export class NetworkService {
|
||||
private _client: WsClient<ServiceType> | null = null;
|
||||
private _state: ENetworkState = ENetworkState.Disconnected;
|
||||
private _clientId: number = 0;
|
||||
private _roomId: string = '';
|
||||
private _callbacks: INetworkCallbacks = {};
|
||||
|
||||
get state(): ENetworkState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
get clientId(): number {
|
||||
return this._clientId;
|
||||
}
|
||||
|
||||
get roomId(): string {
|
||||
return this._roomId;
|
||||
}
|
||||
|
||||
get isConnected(): boolean {
|
||||
return this._state === ENetworkState.Connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置回调
|
||||
* Set callbacks
|
||||
*/
|
||||
setCallbacks(callbacks: INetworkCallbacks): void {
|
||||
this._callbacks = { ...this._callbacks, ...callbacks };
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
* Connect to server
|
||||
*/
|
||||
async connect(serverUrl: string, playerName: string, roomId?: string): Promise<boolean> {
|
||||
if (this._state !== ENetworkState.Disconnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._state = ENetworkState.Connecting;
|
||||
this._client = createClient(serverUrl);
|
||||
this._setupListeners();
|
||||
|
||||
// 连接
|
||||
// Connect
|
||||
const connectResult = await this._client.connect();
|
||||
if (!connectResult.isSucc) {
|
||||
this._state = ENetworkState.Disconnected;
|
||||
this._callbacks.onError?.(new Error(connectResult.errMsg));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 加入房间
|
||||
// Join room
|
||||
const joinResult = await this._client.callApi('Join', {
|
||||
playerName,
|
||||
roomId
|
||||
});
|
||||
|
||||
if (!joinResult.isSucc) {
|
||||
await this._client.disconnect();
|
||||
this._state = ENetworkState.Disconnected;
|
||||
this._callbacks.onError?.(new Error(joinResult.err.message));
|
||||
return false;
|
||||
}
|
||||
|
||||
this._clientId = joinResult.res.clientId;
|
||||
this._roomId = joinResult.res.roomId;
|
||||
this._state = ENetworkState.Connected;
|
||||
this._callbacks.onConnected?.(this._clientId, this._roomId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
* Disconnect
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this._client) {
|
||||
await this._client.disconnect();
|
||||
}
|
||||
this._state = ENetworkState.Disconnected;
|
||||
this._clientId = 0;
|
||||
this._roomId = '';
|
||||
this._client = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送输入
|
||||
* Send input
|
||||
*/
|
||||
sendInput(input: IPlayerInput): void {
|
||||
if (!this.isConnected || !this._client) return;
|
||||
this._client.sendMsg('Input', { input });
|
||||
}
|
||||
|
||||
private _setupListeners(): void {
|
||||
if (!this._client) return;
|
||||
|
||||
this._client.listenMsg('Sync', (msg) => {
|
||||
this._callbacks.onSync?.(msg);
|
||||
});
|
||||
|
||||
this._client.listenMsg('Spawn', (msg) => {
|
||||
this._callbacks.onSpawn?.(msg);
|
||||
});
|
||||
|
||||
this._client.listenMsg('Despawn', (msg) => {
|
||||
this._callbacks.onDespawn?.(msg);
|
||||
});
|
||||
|
||||
this._client.flows.postDisconnectFlow.push((v) => {
|
||||
this._state = ENetworkState.Disconnected;
|
||||
this._callbacks.onDisconnected?.();
|
||||
return v;
|
||||
});
|
||||
}
|
||||
}
|
||||
73
packages/network/src/systems/NetworkInputSystem.ts
Normal file
73
packages/network/src/systems/NetworkInputSystem.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
import type { IPlayerInput } from '@esengine/network-protocols';
|
||||
import type { NetworkService } from '../services/NetworkService';
|
||||
|
||||
/**
|
||||
* 网络输入系统
|
||||
* Network input system
|
||||
*
|
||||
* 收集本地玩家输入并发送到服务器。
|
||||
* Collects local player input and sends to server.
|
||||
*/
|
||||
export class NetworkInputSystem extends EntitySystem {
|
||||
private _networkService: NetworkService;
|
||||
private _frame: number = 0;
|
||||
private _inputQueue: IPlayerInput[] = [];
|
||||
|
||||
constructor(networkService: NetworkService) {
|
||||
// 不查询任何实体,此系统只处理输入
|
||||
// Don't query any entities, this system only handles input
|
||||
super(Matcher.nothing());
|
||||
this._networkService = networkService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理输入队列
|
||||
* Process input queue
|
||||
*/
|
||||
protected override process(): void {
|
||||
if (!this._networkService.isConnected) return;
|
||||
|
||||
this._frame++;
|
||||
|
||||
// 发送队列中的输入
|
||||
// Send queued inputs
|
||||
while (this._inputQueue.length > 0) {
|
||||
const input = this._inputQueue.shift()!;
|
||||
input.frame = this._frame;
|
||||
this._networkService.sendInput(input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加移动输入
|
||||
* Add move input
|
||||
*/
|
||||
public addMoveInput(x: number, y: number): void {
|
||||
this._inputQueue.push({
|
||||
frame: 0,
|
||||
moveDir: { x, y }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加动作输入
|
||||
* Add action input
|
||||
*/
|
||||
public addActionInput(action: string): void {
|
||||
const lastInput = this._inputQueue[this._inputQueue.length - 1];
|
||||
if (lastInput) {
|
||||
lastInput.actions = lastInput.actions || [];
|
||||
lastInput.actions.push(action);
|
||||
} else {
|
||||
this._inputQueue.push({
|
||||
frame: 0,
|
||||
actions: [action]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this._inputQueue.length = 0;
|
||||
}
|
||||
}
|
||||
101
packages/network/src/systems/NetworkSpawnSystem.ts
Normal file
101
packages/network/src/systems/NetworkSpawnSystem.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { EntitySystem, Entity, type Scene, Matcher } from '@esengine/ecs-framework';
|
||||
import type { MsgSpawn, MsgDespawn } from '@esengine/network-protocols';
|
||||
import { NetworkIdentity } from '../components/NetworkIdentity';
|
||||
import { NetworkTransform } from '../components/NetworkTransform';
|
||||
import type { NetworkService } from '../services/NetworkService';
|
||||
import type { NetworkSyncSystem } from './NetworkSyncSystem';
|
||||
|
||||
/**
|
||||
* 预制体工厂函数类型
|
||||
* Prefab factory function type
|
||||
*/
|
||||
export type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
|
||||
|
||||
/**
|
||||
* 网络生成系统
|
||||
* Network spawn system
|
||||
*
|
||||
* 处理网络实体的生成和销毁。
|
||||
* Handles spawning and despawning of networked entities.
|
||||
*/
|
||||
export class NetworkSpawnSystem extends EntitySystem {
|
||||
private _networkService: NetworkService;
|
||||
private _syncSystem: NetworkSyncSystem;
|
||||
private _prefabFactories: Map<string, PrefabFactory> = new Map();
|
||||
|
||||
constructor(networkService: NetworkService, syncSystem: NetworkSyncSystem) {
|
||||
// 不查询任何实体,此系统只响应网络消息
|
||||
// Don't query any entities, this system only responds to network messages
|
||||
super(Matcher.nothing());
|
||||
this._networkService = networkService;
|
||||
this._syncSystem = syncSystem;
|
||||
}
|
||||
|
||||
protected override onInitialize(): void {
|
||||
this._networkService.setCallbacks({
|
||||
onSpawn: this._handleSpawn.bind(this),
|
||||
onDespawn: this._handleDespawn.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册预制体工厂
|
||||
* Register prefab factory
|
||||
*/
|
||||
public registerPrefab(prefabType: string, factory: PrefabFactory): void {
|
||||
this._prefabFactories.set(prefabType, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销预制体工厂
|
||||
* Unregister prefab factory
|
||||
*/
|
||||
public unregisterPrefab(prefabType: string): void {
|
||||
this._prefabFactories.delete(prefabType);
|
||||
}
|
||||
|
||||
private _handleSpawn(msg: MsgSpawn): void {
|
||||
if (!this.scene) return;
|
||||
|
||||
const factory = this._prefabFactories.get(msg.prefab);
|
||||
if (!factory) {
|
||||
this.logger.warn(`Unknown prefab: ${msg.prefab}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = factory(this.scene, msg);
|
||||
|
||||
// 添加网络组件
|
||||
// Add network components
|
||||
const identity = entity.addComponent(new NetworkIdentity());
|
||||
identity.netId = msg.netId;
|
||||
identity.ownerId = msg.ownerId;
|
||||
identity.prefabType = msg.prefab;
|
||||
identity.bHasAuthority = msg.ownerId === this._networkService.clientId;
|
||||
identity.bIsLocalPlayer = identity.bHasAuthority;
|
||||
|
||||
const transform = entity.addComponent(new NetworkTransform());
|
||||
transform.setTarget(msg.pos.x, msg.pos.y, msg.rot);
|
||||
transform.snap();
|
||||
|
||||
// 注册到同步系统
|
||||
// Register to sync system
|
||||
this._syncSystem.registerEntity(msg.netId, entity.id);
|
||||
}
|
||||
|
||||
private _handleDespawn(msg: MsgDespawn): void {
|
||||
const entityId = this._syncSystem.getEntityId(msg.netId);
|
||||
if (entityId === undefined) return;
|
||||
|
||||
const entity = this.scene?.findEntityById(entityId);
|
||||
if (entity) {
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
this._syncSystem.unregisterEntity(msg.netId);
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this._prefabFactories.clear();
|
||||
}
|
||||
}
|
||||
104
packages/network/src/systems/NetworkSyncSystem.ts
Normal file
104
packages/network/src/systems/NetworkSyncSystem.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { EntitySystem, Matcher, Time, type Entity } from '@esengine/ecs-framework';
|
||||
import type { MsgSync } from '@esengine/network-protocols';
|
||||
import { NetworkIdentity } from '../components/NetworkIdentity';
|
||||
import { NetworkTransform } from '../components/NetworkTransform';
|
||||
import type { NetworkService } from '../services/NetworkService';
|
||||
|
||||
/**
|
||||
* 网络同步系统
|
||||
* Network sync system
|
||||
*
|
||||
* 处理网络实体的状态同步和插值。
|
||||
* Handles state synchronization and interpolation for networked entities.
|
||||
*/
|
||||
export class NetworkSyncSystem extends EntitySystem {
|
||||
private _networkService: NetworkService;
|
||||
private _netIdToEntity: Map<number, number> = new Map();
|
||||
|
||||
constructor(networkService: NetworkService) {
|
||||
super(Matcher.all(NetworkIdentity, NetworkTransform));
|
||||
this._networkService = networkService;
|
||||
}
|
||||
|
||||
protected override onInitialize(): void {
|
||||
this._networkService.setCallbacks({
|
||||
onSync: this._handleSync.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理实体列表
|
||||
* Process entities
|
||||
*/
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
for (const entity of entities) {
|
||||
const transform = this.requireComponent(entity, NetworkTransform);
|
||||
const identity = this.requireComponent(entity, NetworkIdentity);
|
||||
|
||||
// 只有非本地玩家需要插值
|
||||
// Only non-local players need interpolation
|
||||
if (!identity.bHasAuthority && transform.bInterpolate) {
|
||||
this._interpolate(transform, deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册网络实体
|
||||
* Register network entity
|
||||
*/
|
||||
public registerEntity(netId: number, entityId: number): void {
|
||||
this._netIdToEntity.set(netId, entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销网络实体
|
||||
* Unregister network entity
|
||||
*/
|
||||
public unregisterEntity(netId: number): void {
|
||||
this._netIdToEntity.delete(netId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据网络 ID 获取实体 ID
|
||||
* Get entity ID by network ID
|
||||
*/
|
||||
public getEntityId(netId: number): number | undefined {
|
||||
return this._netIdToEntity.get(netId);
|
||||
}
|
||||
|
||||
private _handleSync(msg: MsgSync): void {
|
||||
for (const state of msg.entities) {
|
||||
const entityId = this._netIdToEntity.get(state.netId);
|
||||
if (entityId === undefined) continue;
|
||||
|
||||
const entity = this.scene?.findEntityById(entityId);
|
||||
if (!entity) continue;
|
||||
|
||||
const transform = entity.getComponent(NetworkTransform);
|
||||
if (transform && state.pos) {
|
||||
transform.setTarget(state.pos.x, state.pos.y, state.rot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _interpolate(transform: NetworkTransform, deltaTime: number): void {
|
||||
const t = Math.min(1, transform.lerpSpeed * deltaTime);
|
||||
|
||||
transform.currentX += (transform.targetX - transform.currentX) * t;
|
||||
transform.currentY += (transform.targetY - transform.currentY) * t;
|
||||
|
||||
// 角度插值需要处理环绕
|
||||
// Angle interpolation needs to handle wrap-around
|
||||
let angleDiff = transform.targetRotation - transform.currentRotation;
|
||||
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
|
||||
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
|
||||
transform.currentRotation += angleDiff * t;
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this._netIdToEntity.clear();
|
||||
}
|
||||
}
|
||||
38
packages/network/src/tokens.ts
Normal file
38
packages/network/src/tokens.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Network 模块服务令牌
|
||||
* Network module service tokens
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/ecs-framework';
|
||||
import type { NetworkService } from './services/NetworkService';
|
||||
import type { NetworkSyncSystem } from './systems/NetworkSyncSystem';
|
||||
import type { NetworkSpawnSystem } from './systems/NetworkSpawnSystem';
|
||||
import type { NetworkInputSystem } from './systems/NetworkInputSystem';
|
||||
|
||||
// ============================================================================
|
||||
// Network 模块导出的令牌 | Tokens exported by Network module
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 网络服务令牌
|
||||
* Network service token
|
||||
*/
|
||||
export const NetworkServiceToken = createServiceToken<NetworkService>('networkService');
|
||||
|
||||
/**
|
||||
* 网络同步系统令牌
|
||||
* Network sync system token
|
||||
*/
|
||||
export const NetworkSyncSystemToken = createServiceToken<NetworkSyncSystem>('networkSyncSystem');
|
||||
|
||||
/**
|
||||
* 网络生成系统令牌
|
||||
* Network spawn system token
|
||||
*/
|
||||
export const NetworkSpawnSystemToken = createServiceToken<NetworkSpawnSystem>('networkSpawnSystem');
|
||||
|
||||
/**
|
||||
* 网络输入系统令牌
|
||||
* Network input system token
|
||||
*/
|
||||
export const NetworkInputSystemToken = createServiceToken<NetworkInputSystem>('networkInputSystem');
|
||||
13
packages/network/tsconfig.build.json
Normal file
13
packages/network/tsconfig.build.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
15
packages/network/tsconfig.json
Normal file
15
packages/network/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../network-protocols" }
|
||||
]
|
||||
}
|
||||
14
packages/network/tsup.config.ts
Normal file
14
packages/network/tsup.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset({
|
||||
external: [/^tsrpc/, 'tsbuffer', 'tsbuffer-schema']
|
||||
}),
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
// 禁用 tsup 的 DTS 捆绑器,改用 tsc 生成声明文件
|
||||
// tsup 的 DTS bundler 无法正确解析 tsrpc 的类型继承链
|
||||
// Disable tsup's DTS bundler, use tsc to generate declarations
|
||||
// tsup's DTS bundler cannot correctly resolve tsrpc's type inheritance chain
|
||||
dts: false
|
||||
});
|
||||
Reference in New Issue
Block a user