feat(network): 添加状态同步和客户端预测模块 (#330)
* feat(network): 添加状态同步和客户端预测模块 - 添加状态快照接口和快照缓冲区实现 - 实现线性插值和赫尔米特插值器 - 实现客户端预测和服务器校正系统 - 添加网络相关蓝图节点 (IsLocalPlayer, IsServer 等) * chore: update pnpm-lock.yaml
This commit is contained in:
@@ -32,6 +32,7 @@
|
|||||||
"tsrpc-browser": "^3.4.16"
|
"tsrpc-browser": "^3.4.16"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@esengine/blueprint": "workspace:*",
|
||||||
"@esengine/ecs-framework": "workspace:*",
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
"@esengine/build-config": "workspace:*",
|
"@esengine/build-config": "workspace:*",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
|
|||||||
@@ -63,3 +63,56 @@ export { NetworkSyncSystem } from './systems/NetworkSyncSystem';
|
|||||||
export { NetworkSpawnSystem } from './systems/NetworkSpawnSystem';
|
export { NetworkSpawnSystem } from './systems/NetworkSpawnSystem';
|
||||||
export type { PrefabFactory } from './systems/NetworkSpawnSystem';
|
export type { PrefabFactory } from './systems/NetworkSpawnSystem';
|
||||||
export { NetworkInputSystem } from './systems/NetworkInputSystem';
|
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/network/src/nodes/NetworkNodes.ts
Normal file
308
packages/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/network/src/nodes/index.ts
Normal file
24
packages/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';
|
||||||
279
packages/network/src/sync/ClientPrediction.ts
Normal file
279
packages/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/network/src/sync/IInterpolator.ts
Normal file
109
packages/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/network/src/sync/IStateSnapshot.ts
Normal file
138
packages/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/network/src/sync/SnapshotBuffer.ts
Normal file
145
packages/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/network/src/sync/TransformInterpolator.ts
Normal file
121
packages/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/network/src/sync/index.ts
Normal file
48
packages/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';
|
||||||
@@ -9,5 +9,8 @@
|
|||||||
"moduleResolution": "node"
|
"moduleResolution": "node"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
"exclude": ["node_modules", "dist", "**/*.test.ts"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../blueprint" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"exclude": ["node_modules", "dist"],
|
"exclude": ["node_modules", "dist"],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../core" },
|
{ "path": "../core" },
|
||||||
{ "path": "../network-protocols" }
|
{ "path": "../network-protocols" },
|
||||||
|
{ "path": "../blueprint" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -1238,6 +1238,9 @@ importers:
|
|||||||
specifier: ^3.4.16
|
specifier: ^3.4.16
|
||||||
version: 3.4.18
|
version: 3.4.18
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@esengine/blueprint':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../blueprint
|
||||||
'@esengine/build-config':
|
'@esengine/build-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../build-config
|
version: link:../build-config
|
||||||
|
|||||||
Reference in New Issue
Block a user