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:
153
packages/framework/network/src/NetworkPlugin.ts
Normal file
153
packages/framework/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/framework/network/src/components/NetworkIdentity.ts
Normal file
70
packages/framework/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/framework/network/src/components/NetworkTransform.ts
Normal file
100
packages/framework/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;
|
||||
}
|
||||
}
|
||||
118
packages/framework/network/src/index.ts
Normal file
118
packages/framework/network/src/index.ts
Normal 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';
|
||||
308
packages/framework/network/src/nodes/NetworkNodes.ts
Normal file
308
packages/framework/network/src/nodes/NetworkNodes.ts
Normal 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()]
|
||||
])
|
||||
};
|
||||
24
packages/framework/network/src/nodes/index.ts
Normal file
24
packages/framework/network/src/nodes/index.ts
Normal 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';
|
||||
172
packages/framework/network/src/services/NetworkService.ts
Normal file
172
packages/framework/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;
|
||||
});
|
||||
}
|
||||
}
|
||||
279
packages/framework/network/src/sync/ClientPrediction.ts
Normal file
279
packages/framework/network/src/sync/ClientPrediction.ts
Normal 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);
|
||||
}
|
||||
109
packages/framework/network/src/sync/IInterpolator.ts
Normal file
109
packages/framework/network/src/sync/IInterpolator.ts
Normal 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];
|
||||
}
|
||||
138
packages/framework/network/src/sync/IStateSnapshot.ts
Normal file
138
packages/framework/network/src/sync/IStateSnapshot.ts
Normal 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;
|
||||
}
|
||||
145
packages/framework/network/src/sync/SnapshotBuffer.ts
Normal file
145
packages/framework/network/src/sync/SnapshotBuffer.ts
Normal 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 });
|
||||
}
|
||||
121
packages/framework/network/src/sync/TransformInterpolator.ts
Normal file
121
packages/framework/network/src/sync/TransformInterpolator.ts
Normal 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();
|
||||
}
|
||||
48
packages/framework/network/src/sync/index.ts
Normal file
48
packages/framework/network/src/sync/index.ts
Normal 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';
|
||||
73
packages/framework/network/src/systems/NetworkInputSystem.ts
Normal file
73
packages/framework/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/framework/network/src/systems/NetworkSpawnSystem.ts
Normal file
101
packages/framework/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/framework/network/src/systems/NetworkSyncSystem.ts
Normal file
104
packages/framework/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/framework/network/src/tokens.ts
Normal file
38
packages/framework/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');
|
||||
Reference in New Issue
Block a user