Newer
Older
PixelPaintWar / apps / client / src / scenes / game / application / time / OffsetSmoother.ts
/**
 * OffsetSmoother
 * 時計差分とRTTの平滑化を管理する
 * 外れ値除外とEWMA更新を集中管理する
 */
import type { PongSample } from "@client/scenes/game/application/time/PongSampleEstimator";

/** 平滑化処理の設定値 */
export type OffsetSmootherConfig = {
  offsetAlpha: number;
  rttAlpha: number;
  maxAcceptedOffsetJumpMs: number;
};

/** 平滑化処理の既定設定 */
export const DEFAULT_OFFSET_SMOOTHER_CONFIG: OffsetSmootherConfig = {
  offsetAlpha: 0.12,
  rttAlpha: 0.25,
  maxAcceptedOffsetJumpMs: 250,
};

/** 時計差分とRTTの平滑化状態を保持する */
export class OffsetSmoother {
  private readonly config: OffsetSmootherConfig;
  private smoothedOffsetMs: number | null = null;
  private smoothedRttMs: number | null = null;

  constructor(config: Partial<OffsetSmootherConfig> = {}) {
    this.config = {
      ...DEFAULT_OFFSET_SMOOTHER_CONFIG,
      ...config,
    };
  }

  /** serverNowからoffset初期値を設定する */
  public seed(serverNowMs: number, receivedAtMs: number): void {
    this.smoothedOffsetMs = serverNowMs - receivedAtMs;
  }

  /** 推定サンプルを取り込み平滑化状態を更新する */
  public applySample(sample: PongSample): void {
    this.smoothedRttMs = this.smoothValue(
      this.smoothedRttMs,
      sample.rttMs,
      this.config.rttAlpha,
    );

    if (
      this.smoothedOffsetMs !== null &&
      Math.abs(sample.offsetMs - this.smoothedOffsetMs) >
        this.config.maxAcceptedOffsetJumpMs
    ) {
      return;
    }

    this.smoothedOffsetMs = this.smoothValue(
      this.smoothedOffsetMs,
      sample.offsetMs,
      this.config.offsetAlpha,
    );
  }

  /** 平滑化済みoffsetを返す */
  public getClockOffsetMs(): number {
    return this.smoothedOffsetMs ?? 0;
  }

  /** 平滑化済みRTTを返す */
  public getSmoothedRttMs(): number | null {
    return this.smoothedRttMs;
  }

  /** 内部平滑化状態を初期化する */
  public reset(): void {
    this.smoothedOffsetMs = null;
    this.smoothedRttMs = null;
  }

  private smoothValue(
    current: number | null,
    measured: number,
    alpha: number,
  ): number {
    if (current === null) {
      return measured;
    }

    return current * (1 - alpha) + measured * alpha;
  }
}