* 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
280 lines
8.3 KiB
TypeScript
280 lines
8.3 KiB
TypeScript
/**
|
|
* @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);
|
|
}
|