refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View 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"
}
]
}

View File

@@ -0,0 +1,51 @@
{
"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/blueprint": "workspace:*",
"@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"
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,118 @@
/**
* @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';
// ============================================================================
// State Sync | 状态同步
// ============================================================================
export type {
IStateSnapshot,
ITransformState,
ITransformStateWithVelocity,
ISnapshotBufferConfig,
ISnapshotBuffer
} from './sync';
export type {
IInterpolator,
IExtrapolator,
IInputSnapshot,
IPredictedState,
IPredictor,
ClientPredictionConfig
} from './sync';
export {
lerp,
lerpAngle,
smoothDamp,
SnapshotBuffer,
createSnapshotBuffer,
TransformInterpolator,
HermiteTransformInterpolator,
createTransformInterpolator,
createHermiteTransformInterpolator,
ClientPrediction,
createClientPrediction
} from './sync';
// ============================================================================
// Blueprint Nodes | 蓝图节点
// ============================================================================
export {
IsLocalPlayerTemplate,
IsServerTemplate,
HasAuthorityTemplate,
GetNetworkIdTemplate,
GetLocalPlayerIdTemplate,
IsLocalPlayerExecutor,
IsServerExecutor,
HasAuthorityExecutor,
GetNetworkIdExecutor,
GetLocalPlayerIdExecutor,
NetworkNodeDefinitions
} from './nodes';

View File

@@ -0,0 +1,308 @@
/**
* @zh 网络蓝图节点
* @en Network Blueprint Nodes
*
* @zh 提供网络功能的蓝图节点
* @en Provides blueprint nodes for network functionality
*/
import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint';
// =============================================================================
// 执行上下文接口 | Execution Context Interface
// =============================================================================
/**
* @zh 网络上下文
* @en Network context
*/
interface NetworkContext {
entity: {
getComponent<T>(type: new (...args: unknown[]) => T): T | null;
};
isServer: boolean;
localPlayerId: number;
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
setOutputs(nodeId: string, outputs: Record<string, unknown>): void;
}
// =============================================================================
// IsLocalPlayer 节点 | IsLocalPlayer Node
// =============================================================================
/**
* @zh IsLocalPlayer 节点模板
* @en IsLocalPlayer node template
*/
export const IsLocalPlayerTemplate: BlueprintNodeTemplate = {
type: 'IsLocalPlayer',
title: 'Is Local Player',
category: 'entity',
description: 'Check if this entity is the local player / 检查此实体是否是本地玩家',
keywords: ['network', 'local', 'player', 'authority', 'owner'],
menuPath: ['Network', 'Is Local Player'],
isPure: true,
inputs: [],
outputs: [
{
name: 'isLocal',
displayName: 'Is Local',
type: 'bool'
}
],
color: '#ff9800'
};
/**
* @zh IsLocalPlayer 节点执行器
* @en IsLocalPlayer node executor
*/
export class IsLocalPlayerExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as NetworkContext;
// Try to get NetworkIdentity component
let isLocal = false;
if (ctx.entity) {
const identity = ctx.entity.getComponent(class NetworkIdentity {
bIsLocalPlayer: boolean = false;
});
if (identity) {
isLocal = identity.bIsLocalPlayer;
}
}
return {
outputs: {
isLocal
}
};
}
}
// =============================================================================
// IsServer 节点 | IsServer Node
// =============================================================================
/**
* @zh IsServer 节点模板
* @en IsServer node template
*/
export const IsServerTemplate: BlueprintNodeTemplate = {
type: 'IsServer',
title: 'Is Server',
category: 'entity',
description: 'Check if running on server / 检查是否在服务器上运行',
keywords: ['network', 'server', 'authority', 'host'],
menuPath: ['Network', 'Is Server'],
isPure: true,
inputs: [],
outputs: [
{
name: 'isServer',
displayName: 'Is Server',
type: 'bool'
}
],
color: '#ff9800'
};
/**
* @zh IsServer 节点执行器
* @en IsServer node executor
*/
export class IsServerExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as NetworkContext;
return {
outputs: {
isServer: ctx.isServer ?? false
}
};
}
}
// =============================================================================
// HasAuthority 节点 | HasAuthority Node
// =============================================================================
/**
* @zh HasAuthority 节点模板
* @en HasAuthority node template
*/
export const HasAuthorityTemplate: BlueprintNodeTemplate = {
type: 'HasAuthority',
title: 'Has Authority',
category: 'entity',
description: 'Check if this entity has authority / 检查此实体是否有权限控制',
keywords: ['network', 'authority', 'control', 'owner'],
menuPath: ['Network', 'Has Authority'],
isPure: true,
inputs: [],
outputs: [
{
name: 'hasAuthority',
displayName: 'Has Authority',
type: 'bool'
}
],
color: '#ff9800'
};
/**
* @zh HasAuthority 节点执行器
* @en HasAuthority node executor
*/
export class HasAuthorityExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as NetworkContext;
let hasAuthority = false;
if (ctx.entity) {
const identity = ctx.entity.getComponent(class NetworkIdentity {
bHasAuthority: boolean = false;
});
if (identity) {
hasAuthority = identity.bHasAuthority;
}
}
return {
outputs: {
hasAuthority
}
};
}
}
// =============================================================================
// GetNetworkId 节点 | GetNetworkId Node
// =============================================================================
/**
* @zh GetNetworkId 节点模板
* @en GetNetworkId node template
*/
export const GetNetworkIdTemplate: BlueprintNodeTemplate = {
type: 'GetNetworkId',
title: 'Get Network ID',
category: 'entity',
description: 'Get the network ID of this entity / 获取此实体的网络 ID',
keywords: ['network', 'id', 'netid', 'identity'],
menuPath: ['Network', 'Get Network ID'],
isPure: true,
inputs: [],
outputs: [
{
name: 'netId',
displayName: 'Net ID',
type: 'int'
},
{
name: 'ownerId',
displayName: 'Owner ID',
type: 'int'
}
],
color: '#ff9800'
};
/**
* @zh GetNetworkId 节点执行器
* @en GetNetworkId node executor
*/
export class GetNetworkIdExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as NetworkContext;
let netId = 0;
let ownerId = 0;
if (ctx.entity) {
const identity = ctx.entity.getComponent(class NetworkIdentity {
netId: number = 0;
ownerId: number = 0;
});
if (identity) {
netId = identity.netId;
ownerId = identity.ownerId;
}
}
return {
outputs: {
netId,
ownerId
}
};
}
}
// =============================================================================
// GetLocalPlayerId 节点 | GetLocalPlayerId Node
// =============================================================================
/**
* @zh GetLocalPlayerId 节点模板
* @en GetLocalPlayerId node template
*/
export const GetLocalPlayerIdTemplate: BlueprintNodeTemplate = {
type: 'GetLocalPlayerId',
title: 'Get Local Player ID',
category: 'entity',
description: 'Get the local player ID / 获取本地玩家 ID',
keywords: ['network', 'local', 'player', 'id'],
menuPath: ['Network', 'Get Local Player ID'],
isPure: true,
inputs: [],
outputs: [
{
name: 'playerId',
displayName: 'Player ID',
type: 'int'
}
],
color: '#ff9800'
};
/**
* @zh GetLocalPlayerId 节点执行器
* @en GetLocalPlayerId node executor
*/
export class GetLocalPlayerIdExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as NetworkContext;
return {
outputs: {
playerId: ctx.localPlayerId ?? 0
}
};
}
}
// =============================================================================
// 节点定义集合 | Node Definition Collection
// =============================================================================
/**
* @zh 网络节点定义集合
* @en Network node definition collection
*/
export const NetworkNodeDefinitions = {
templates: [
IsLocalPlayerTemplate,
IsServerTemplate,
HasAuthorityTemplate,
GetNetworkIdTemplate,
GetLocalPlayerIdTemplate
],
executors: new Map<string, INodeExecutor>([
['IsLocalPlayer', new IsLocalPlayerExecutor()],
['IsServer', new IsServerExecutor()],
['HasAuthority', new HasAuthorityExecutor()],
['GetNetworkId', new GetNetworkIdExecutor()],
['GetLocalPlayerId', new GetLocalPlayerIdExecutor()]
])
};

View File

@@ -0,0 +1,24 @@
/**
* @zh 网络蓝图节点模块
* @en Network Blueprint Nodes Module
*
* @zh 提供网络功能的蓝图节点
* @en Provides blueprint nodes for network functionality
*/
export {
// Templates
IsLocalPlayerTemplate,
IsServerTemplate,
HasAuthorityTemplate,
GetNetworkIdTemplate,
GetLocalPlayerIdTemplate,
// Executors
IsLocalPlayerExecutor,
IsServerExecutor,
HasAuthorityExecutor,
GetNetworkIdExecutor,
GetLocalPlayerIdExecutor,
// Collection
NetworkNodeDefinitions
} from './NetworkNodes';

View 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;
});
}
}

View File

@@ -0,0 +1,279 @@
/**
* @zh 客户端预测
* @en Client Prediction
*
* @zh 提供客户端输入预测和服务器校正
* @en Provides client-side input prediction and server reconciliation
*/
// =============================================================================
// 输入快照接口 | Input Snapshot Interface
// =============================================================================
/**
* @zh 输入快照
* @en Input snapshot
*/
export interface IInputSnapshot<TInput> {
/**
* @zh 输入序列号
* @en Input sequence number
*/
readonly sequence: number;
/**
* @zh 输入数据
* @en Input data
*/
readonly input: TInput;
/**
* @zh 输入时间戳
* @en Input timestamp
*/
readonly timestamp: number;
}
/**
* @zh 预测状态
* @en Predicted state
*/
export interface IPredictedState<TState> {
/**
* @zh 状态数据
* @en State data
*/
readonly state: TState;
/**
* @zh 对应的输入序列号
* @en Corresponding input sequence number
*/
readonly sequence: number;
}
// =============================================================================
// 预测器接口 | Predictor Interface
// =============================================================================
/**
* @zh 状态预测器接口
* @en State predictor interface
*/
export interface IPredictor<TState, TInput> {
/**
* @zh 根据当前状态和输入预测下一状态
* @en Predict next state based on current state and input
*
* @param state - @zh 当前状态 @en Current state
* @param input - @zh 输入 @en Input
* @param deltaTime - @zh 时间间隔 @en Delta time
* @returns @zh 预测的状态 @en Predicted state
*/
predict(state: TState, input: TInput, deltaTime: number): TState;
}
// =============================================================================
// 客户端预测管理器 | Client Prediction Manager
// =============================================================================
/**
* @zh 客户端预测配置
* @en Client prediction configuration
*/
export interface ClientPredictionConfig {
/**
* @zh 最大未确认输入数量
* @en Maximum unacknowledged inputs
*/
maxUnacknowledgedInputs: number;
/**
* @zh 校正阈值(超过此值才进行平滑校正)
* @en Reconciliation threshold (smooth correction only above this value)
*/
reconciliationThreshold: number;
/**
* @zh 校正平滑速度
* @en Reconciliation smoothing speed
*/
reconciliationSpeed: number;
}
/**
* @zh 客户端预测管理器
* @en Client prediction manager
*/
export class ClientPrediction<TState, TInput> {
private readonly _predictor: IPredictor<TState, TInput>;
private readonly _config: ClientPredictionConfig;
private readonly _pendingInputs: IInputSnapshot<TInput>[] = [];
private _lastAcknowledgedSequence: number = 0;
private _currentSequence: number = 0;
private _lastServerState: TState | null = null;
private _predictedState: TState | null = null;
private _correctionOffset: { x: number; y: number } = { x: 0, y: 0 };
constructor(predictor: IPredictor<TState, TInput>, config?: Partial<ClientPredictionConfig>) {
this._predictor = predictor;
this._config = {
maxUnacknowledgedInputs: 60,
reconciliationThreshold: 0.1,
reconciliationSpeed: 10,
...config
};
}
/**
* @zh 获取当前预测状态
* @en Get current predicted state
*/
get predictedState(): TState | null {
return this._predictedState;
}
/**
* @zh 获取校正偏移
* @en Get correction offset
*/
get correctionOffset(): { x: number; y: number } {
return this._correctionOffset;
}
/**
* @zh 获取待确认输入数量
* @en Get pending input count
*/
get pendingInputCount(): number {
return this._pendingInputs.length;
}
/**
* @zh 记录并预测输入
* @en Record and predict input
*
* @param input - @zh 输入数据 @en Input data
* @param currentState - @zh 当前状态 @en Current state
* @param deltaTime - @zh 时间间隔 @en Delta time
* @returns @zh 预测的状态 @en Predicted state
*/
recordInput(input: TInput, currentState: TState, deltaTime: number): TState {
this._currentSequence++;
const inputSnapshot: IInputSnapshot<TInput> = {
sequence: this._currentSequence,
input,
timestamp: Date.now()
};
this._pendingInputs.push(inputSnapshot);
// Remove old inputs if buffer is full
while (this._pendingInputs.length > this._config.maxUnacknowledgedInputs) {
this._pendingInputs.shift();
}
// Predict new state
this._predictedState = this._predictor.predict(currentState, input, deltaTime);
return this._predictedState;
}
/**
* @zh 获取下一个要发送的输入
* @en Get next input to send
*/
getInputToSend(): IInputSnapshot<TInput> | null {
return this._pendingInputs.length > 0 ? this._pendingInputs[this._pendingInputs.length - 1] : null;
}
/**
* @zh 获取当前序列号
* @en Get current sequence number
*/
get currentSequence(): number {
return this._currentSequence;
}
/**
* @zh 处理服务器状态并进行校正
* @en Process server state and reconcile
*
* @param serverState - @zh 服务器状态 @en Server state
* @param acknowledgedSequence - @zh 已确认的输入序列号 @en Acknowledged input sequence
* @param stateGetter - @zh 获取状态位置的函数 @en Function to get state position
* @param deltaTime - @zh 帧时间 @en Frame delta time
*/
reconcile(
serverState: TState,
acknowledgedSequence: number,
stateGetter: (state: TState) => { x: number; y: number },
deltaTime: number
): TState {
this._lastServerState = serverState;
this._lastAcknowledgedSequence = acknowledgedSequence;
// Remove acknowledged inputs
while (this._pendingInputs.length > 0 && this._pendingInputs[0].sequence <= acknowledgedSequence) {
this._pendingInputs.shift();
}
// Re-predict from server state using unacknowledged inputs
let state = serverState;
for (const inputSnapshot of this._pendingInputs) {
state = this._predictor.predict(state, inputSnapshot.input, deltaTime);
}
// Calculate error
const serverPos = stateGetter(serverState);
const predictedPos = stateGetter(state);
const errorX = serverPos.x - predictedPos.x;
const errorY = serverPos.y - predictedPos.y;
const errorMagnitude = Math.sqrt(errorX * errorX + errorY * errorY);
// Apply correction
if (errorMagnitude > this._config.reconciliationThreshold) {
// Smooth correction over time
const t = Math.min(1, this._config.reconciliationSpeed * deltaTime);
this._correctionOffset.x += errorX * t;
this._correctionOffset.y += errorY * t;
}
// Decay correction offset
const decayRate = 0.9;
this._correctionOffset.x *= decayRate;
this._correctionOffset.y *= decayRate;
this._predictedState = state;
return state;
}
/**
* @zh 清空预测状态
* @en Clear prediction state
*/
clear(): void {
this._pendingInputs.length = 0;
this._lastAcknowledgedSequence = 0;
this._currentSequence = 0;
this._lastServerState = null;
this._predictedState = null;
this._correctionOffset = { x: 0, y: 0 };
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh 创建客户端预测管理器
* @en Create client prediction manager
*/
export function createClientPrediction<TState, TInput>(
predictor: IPredictor<TState, TInput>,
config?: Partial<ClientPredictionConfig>
): ClientPrediction<TState, TInput> {
return new ClientPrediction(predictor, config);
}

View File

@@ -0,0 +1,109 @@
/**
* @zh 插值器接口
* @en Interpolator Interface
*
* @zh 提供状态插值的抽象
* @en Provides abstraction for state interpolation
*/
// =============================================================================
// 插值器接口 | Interpolator Interface
// =============================================================================
/**
* @zh 插值器接口
* @en Interpolator interface
*/
export interface IInterpolator<T> {
/**
* @zh 在两个状态之间插值
* @en Interpolate between two states
*
* @param from - @zh 起始状态 @en Start state
* @param to - @zh 目标状态 @en Target state
* @param t - @zh 插值因子 (0-1) @en Interpolation factor (0-1)
* @returns @zh 插值后的状态 @en Interpolated state
*/
interpolate(from: T, to: T, t: number): T;
}
/**
* @zh 外推器接口
* @en Extrapolator interface
*/
export interface IExtrapolator<T> {
/**
* @zh 基于当前状态外推
* @en Extrapolate based on current state
*
* @param state - @zh 当前状态 @en Current state
* @param deltaTime - @zh 外推时间(秒)@en Extrapolation time in seconds
* @returns @zh 外推后的状态 @en Extrapolated state
*/
extrapolate(state: T, deltaTime: number): T;
}
// =============================================================================
// 内置插值器 | Built-in Interpolators
// =============================================================================
/**
* @zh 线性插值函数
* @en Linear interpolation function
*/
export function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
/**
* @zh 角度插值函数(处理环绕)
* @en Angle interpolation function (handles wrap-around)
*/
export function lerpAngle(a: number, b: number, t: number): number {
let diff = b - a;
while (diff > Math.PI) diff -= Math.PI * 2;
while (diff < -Math.PI) diff += Math.PI * 2;
return a + diff * t;
}
/**
* @zh 平滑阻尼插值
* @en Smooth damp interpolation
*
* @param current - @zh 当前值 @en Current value
* @param target - @zh 目标值 @en Target value
* @param velocity - @zh 当前速度(将被修改)@en Current velocity (will be modified)
* @param smoothTime - @zh 平滑时间 @en Smooth time
* @param deltaTime - @zh 帧时间 @en Delta time
* @param maxSpeed - @zh 最大速度 @en Maximum speed
* @returns @zh [新值, 新速度] @en [new value, new velocity]
*/
export function smoothDamp(
current: number,
target: number,
velocity: number,
smoothTime: number,
deltaTime: number,
maxSpeed: number = Infinity
): [number, number] {
smoothTime = Math.max(0.0001, smoothTime);
const omega = 2 / smoothTime;
const x = omega * deltaTime;
const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
let change = current - target;
const maxChange = maxSpeed * smoothTime;
change = Math.max(-maxChange, Math.min(maxChange, change));
const temp = (velocity + omega * change) * deltaTime;
let newVelocity = (velocity - omega * temp) * exp;
let newValue = target + (change + temp) * exp;
// Prevent overshoot
if ((target - current > 0) === (newValue > target)) {
newValue = target;
newVelocity = (newValue - target) / deltaTime;
}
return [newValue, newVelocity];
}

View File

@@ -0,0 +1,138 @@
/**
* @zh 状态快照接口
* @en State Snapshot Interface
*
* @zh 提供网络同步的状态快照抽象
* @en Provides state snapshot abstraction for network synchronization
*/
// =============================================================================
// 快照接口 | Snapshot Interface
// =============================================================================
/**
* @zh 带时间戳的状态快照
* @en Timestamped state snapshot
*/
export interface IStateSnapshot<T> {
/**
* @zh 服务器时间戳(毫秒)
* @en Server timestamp in milliseconds
*/
readonly timestamp: number;
/**
* @zh 状态数据
* @en State data
*/
readonly state: T;
}
/**
* @zh 变换状态
* @en Transform state
*/
export interface ITransformState {
/**
* @zh X 坐标
* @en X coordinate
*/
x: number;
/**
* @zh Y 坐标
* @en Y coordinate
*/
y: number;
/**
* @zh 旋转角度(弧度)
* @en Rotation angle in radians
*/
rotation: number;
}
/**
* @zh 带速度的变换状态
* @en Transform state with velocity
*/
export interface ITransformStateWithVelocity extends ITransformState {
/**
* @zh X 速度
* @en X velocity
*/
velocityX: number;
/**
* @zh Y 速度
* @en Y velocity
*/
velocityY: number;
/**
* @zh 角速度
* @en Angular velocity
*/
angularVelocity: number;
}
// =============================================================================
// 快照缓冲区接口 | Snapshot Buffer Interface
// =============================================================================
/**
* @zh 快照缓冲区配置
* @en Snapshot buffer configuration
*/
export interface ISnapshotBufferConfig {
/**
* @zh 缓冲区最大大小
* @en Maximum buffer size
*/
maxSize: number;
/**
* @zh 插值延迟(毫秒)
* @en Interpolation delay in milliseconds
*/
interpolationDelay: number;
}
/**
* @zh 快照缓冲区接口
* @en Snapshot buffer interface
*/
export interface ISnapshotBuffer<T> {
/**
* @zh 添加快照
* @en Add snapshot
*/
push(snapshot: IStateSnapshot<T>): void;
/**
* @zh 获取用于插值的两个快照
* @en Get two snapshots for interpolation
*
* @param renderTime - @zh 渲染时间 @en Render time
* @returns @zh [前一个快照, 后一个快照, 插值因子] 或 null @en [previous, next, factor] or null
*/
getInterpolationSnapshots(renderTime: number): [IStateSnapshot<T>, IStateSnapshot<T>, number] | null;
/**
* @zh 获取最新快照
* @en Get latest snapshot
*/
getLatest(): IStateSnapshot<T> | null;
/**
* @zh 获取缓冲区大小
* @en Get buffer size
*/
readonly size: number;
/**
* @zh 清空缓冲区
* @en Clear buffer
*/
clear(): void;
}

View File

@@ -0,0 +1,145 @@
/**
* @zh 快照缓冲区实现
* @en Snapshot Buffer Implementation
*
* @zh 用于存储和插值网络状态快照
* @en Stores and interpolates network state snapshots
*/
import type { IStateSnapshot, ISnapshotBuffer, ISnapshotBufferConfig } from './IStateSnapshot';
// =============================================================================
// 快照缓冲区实现 | Snapshot Buffer Implementation
// =============================================================================
/**
* @zh 快照缓冲区
* @en Snapshot buffer
*/
export class SnapshotBuffer<T> implements ISnapshotBuffer<T> {
private readonly _buffer: IStateSnapshot<T>[] = [];
private readonly _maxSize: number;
private readonly _interpolationDelay: number;
constructor(config: ISnapshotBufferConfig) {
this._maxSize = config.maxSize;
this._interpolationDelay = config.interpolationDelay;
}
get size(): number {
return this._buffer.length;
}
/**
* @zh 获取插值延迟
* @en Get interpolation delay
*/
get interpolationDelay(): number {
return this._interpolationDelay;
}
/**
* @zh 添加快照
* @en Add snapshot
*/
push(snapshot: IStateSnapshot<T>): void {
// Insert in sorted order by timestamp
let insertIndex = this._buffer.length;
for (let i = this._buffer.length - 1; i >= 0; i--) {
if (this._buffer[i].timestamp <= snapshot.timestamp) {
insertIndex = i + 1;
break;
}
if (i === 0) {
insertIndex = 0;
}
}
this._buffer.splice(insertIndex, 0, snapshot);
// Remove old snapshots if buffer is full
while (this._buffer.length > this._maxSize) {
this._buffer.shift();
}
}
/**
* @zh 获取用于插值的两个快照
* @en Get two snapshots for interpolation
*/
getInterpolationSnapshots(renderTime: number): [IStateSnapshot<T>, IStateSnapshot<T>, number] | null {
if (this._buffer.length < 2) {
return null;
}
// Apply interpolation delay
const targetTime = renderTime - this._interpolationDelay;
// Find the two snapshots that bracket the target time
for (let i = 0; i < this._buffer.length - 1; i++) {
const prev = this._buffer[i];
const next = this._buffer[i + 1];
if (prev.timestamp <= targetTime && next.timestamp >= targetTime) {
const duration = next.timestamp - prev.timestamp;
const t = duration > 0 ? (targetTime - prev.timestamp) / duration : 0;
return [prev, next, Math.max(0, Math.min(1, t))];
}
}
// If target time is beyond buffer, extrapolate from last two snapshots
if (targetTime > this._buffer[this._buffer.length - 1].timestamp) {
const prev = this._buffer[this._buffer.length - 2];
const next = this._buffer[this._buffer.length - 1];
const duration = next.timestamp - prev.timestamp;
const t = duration > 0 ? (targetTime - prev.timestamp) / duration : 1;
// Clamp extrapolation to prevent wild values
return [prev, next, Math.min(t, 2)];
}
// Target time is before buffer start
return null;
}
/**
* @zh 获取最新快照
* @en Get latest snapshot
*/
getLatest(): IStateSnapshot<T> | null {
return this._buffer.length > 0 ? this._buffer[this._buffer.length - 1] : null;
}
/**
* @zh 获取特定时间之后的所有快照
* @en Get all snapshots after a specific time
*/
getSnapshotsAfter(timestamp: number): IStateSnapshot<T>[] {
return this._buffer.filter(s => s.timestamp > timestamp);
}
/**
* @zh 清空缓冲区
* @en Clear buffer
*/
clear(): void {
this._buffer.length = 0;
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh 创建快照缓冲区
* @en Create snapshot buffer
*
* @param maxSize - @zh 最大快照数量(默认 30@en Maximum snapshot count (default 30)
* @param interpolationDelay - @zh 插值延迟毫秒(默认 100@en Interpolation delay in ms (default 100)
*/
export function createSnapshotBuffer<T>(
maxSize: number = 30,
interpolationDelay: number = 100
): SnapshotBuffer<T> {
return new SnapshotBuffer<T>({ maxSize, interpolationDelay });
}

View File

@@ -0,0 +1,121 @@
/**
* @zh 变换插值器
* @en Transform Interpolator
*
* @zh 用于网络变换状态的插值
* @en Interpolates network transform states
*/
import type { ITransformState, ITransformStateWithVelocity } from './IStateSnapshot';
import type { IInterpolator, IExtrapolator } from './IInterpolator';
import { lerp, lerpAngle } from './IInterpolator';
// =============================================================================
// 变换插值器 | Transform Interpolator
// =============================================================================
/**
* @zh 变换状态插值器
* @en Transform state interpolator
*/
export class TransformInterpolator implements IInterpolator<ITransformState>, IExtrapolator<ITransformStateWithVelocity> {
/**
* @zh 在两个变换状态之间插值
* @en Interpolate between two transform states
*/
interpolate(from: ITransformState, to: ITransformState, t: number): ITransformState {
return {
x: lerp(from.x, to.x, t),
y: lerp(from.y, to.y, t),
rotation: lerpAngle(from.rotation, to.rotation, t)
};
}
/**
* @zh 基于速度外推变换状态
* @en Extrapolate transform state based on velocity
*/
extrapolate(state: ITransformStateWithVelocity, deltaTime: number): ITransformStateWithVelocity {
return {
x: state.x + state.velocityX * deltaTime,
y: state.y + state.velocityY * deltaTime,
rotation: state.rotation + state.angularVelocity * deltaTime,
velocityX: state.velocityX,
velocityY: state.velocityY,
angularVelocity: state.angularVelocity
};
}
}
// =============================================================================
// 赫尔米特插值器 | Hermite Interpolator
// =============================================================================
/**
* @zh 赫尔米特变换插值器(更平滑的曲线)
* @en Hermite transform interpolator (smoother curves)
*/
export class HermiteTransformInterpolator implements IInterpolator<ITransformStateWithVelocity> {
/**
* @zh 使用赫尔米特插值
* @en Use Hermite interpolation
*/
interpolate(
from: ITransformStateWithVelocity,
to: ITransformStateWithVelocity,
t: number
): ITransformStateWithVelocity {
const t2 = t * t;
const t3 = t2 * t;
// Hermite basis functions
const h00 = 2 * t3 - 3 * t2 + 1;
const h10 = t3 - 2 * t2 + t;
const h01 = -2 * t3 + 3 * t2;
const h11 = t3 - t2;
// Estimate time interval (assume 100ms between snapshots)
const dt = 0.1;
const x = h00 * from.x + h10 * from.velocityX * dt + h01 * to.x + h11 * to.velocityX * dt;
const y = h00 * from.y + h10 * from.velocityY * dt + h01 * to.y + h11 * to.velocityY * dt;
// Derive velocity from position derivatives
const dh00 = 6 * t2 - 6 * t;
const dh10 = 3 * t2 - 4 * t + 1;
const dh01 = -6 * t2 + 6 * t;
const dh11 = 3 * t2 - 2 * t;
const velocityX = (dh00 * from.x + dh10 * from.velocityX * dt + dh01 * to.x + dh11 * to.velocityX * dt) / dt;
const velocityY = (dh00 * from.y + dh10 * from.velocityY * dt + dh01 * to.y + dh11 * to.velocityY * dt) / dt;
return {
x,
y,
rotation: lerpAngle(from.rotation, to.rotation, t),
velocityX,
velocityY,
angularVelocity: lerp(from.angularVelocity, to.angularVelocity, t)
};
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh 创建变换插值器
* @en Create transform interpolator
*/
export function createTransformInterpolator(): TransformInterpolator {
return new TransformInterpolator();
}
/**
* @zh 创建赫尔米特变换插值器
* @en Create Hermite transform interpolator
*/
export function createHermiteTransformInterpolator(): HermiteTransformInterpolator {
return new HermiteTransformInterpolator();
}

View File

@@ -0,0 +1,48 @@
/**
* @zh 网络同步模块
* @en Network Sync Module
*
* @zh 提供状态快照、插值和客户端预测功能
* @en Provides state snapshot, interpolation, and client prediction functionality
*/
// =============================================================================
// 状态快照 | State Snapshot
// =============================================================================
export type {
IStateSnapshot,
ITransformState,
ITransformStateWithVelocity,
ISnapshotBufferConfig,
ISnapshotBuffer
} from './IStateSnapshot';
export { SnapshotBuffer, createSnapshotBuffer } from './SnapshotBuffer';
// =============================================================================
// 插值器 | Interpolators
// =============================================================================
export type { IInterpolator, IExtrapolator } from './IInterpolator';
export { lerp, lerpAngle, smoothDamp } from './IInterpolator';
export {
TransformInterpolator,
HermiteTransformInterpolator,
createTransformInterpolator,
createHermiteTransformInterpolator
} from './TransformInterpolator';
// =============================================================================
// 客户端预测 | Client Prediction
// =============================================================================
export type {
IInputSnapshot,
IPredictedState,
IPredictor,
ClientPredictionConfig
} from './ClientPrediction';
export { ClientPrediction, createClientPrediction } from './ClientPrediction';

View 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;
}
}

View 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();
}
}

View 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();
}
}

View 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');

View File

@@ -0,0 +1,16 @@
{
"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"],
"references": [
{ "path": "../blueprint" }
]
}

View File

@@ -0,0 +1,27 @@
{
"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"
},
{
"path": "../blueprint"
}
]
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'tsup';
import { runtimeOnlyPreset } from '../../tools/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
});