Files
esengine/packages/framework/network/src/sync/ClientPrediction.ts

280 lines
8.3 KiB
TypeScript
Raw Normal View History

/**
* @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);
}