Source: ./src/engine/utils/timer.js

/**
 * 게임에서 1 프레임마다 캐릭터가 10픽셀씩 이동한다고 가정하자.
 * 60fps일 때는 1초에 60번 게임이 업데이트되므로,
 * 10픽셀 * 60 = 600픽셀만큼 캐릭터가 이동하게 된다.
 *
 * 반면 144fps일때는 1초에 144번 게임이 업데이트되므로,
 * 10픽셀 * 144 = 1440픽셀만큼 캐릭터가 이동하게 된다.
 * 즉 프레임에 의해 캐릭터가 이동하는 거리가 달라지게 된다.
 *
 * fps에 따라서 캐릭터가 이동하는 거리가 달라지므로,
 * 정확한 이동거리를 보장할 수 없다.
 * 이를 보정하는 방법이 이동속도에 deltaTime을 곱하는 방법이다.
 *
 * 이 객체에서는 각 프레임간 시간차를 제공한다.
 * 엔진에서 매 update때마다 이 객체의 deltaTime을 하위의 오브젝트들에게 전달하여
 * 정확한 이동거리를 보장할 수 있게 한다.
 *
 * deltaTime에 대한 설명은 아래 게시글에서 찾아볼 수 있습니다.
 * https://bluemeta.tistory.com/1
 *
 * deltaTime은 이전 프레임과 현재 프레임간의 시간 차이다.
 *
 *  frame     0         0.4   0.6             1
 *            +----------+-----+--------------+
 * deltaTime  |    0.4   | 0.2 |      0.4     |
 *
 * 위 그림에서 보이듯이 게임이 0.4초, 0.6초, 1초에 업데이트된다고 가정하자.
 * 캐릭터가 정확하게 100픽셀을 움직여야 한다고 하자.
 * 이 때 게임이 3번 업데이트되므로, 300픽셀을 움직이게 된다.
 * 하지만 업데이트될 때마다 deltaTime을 곱하면 아래처럼 정확히 100픽셀을 움직인다.
 *
 *  frame   deltaTime      이동 거리
 * -------+-----------+--------------
 *   0.4s |      0.4s | 100 x 0.4 = 40
 *   0.6s |      0.2s | 100 x 0.2 = 20
 *   1.0s |      0.4s | 100 x 0.4 = 40
 *
 * 이동거리를 모두 더하면 100픽셀이 된다.
 *
 * fps가 항상 고정된 수치는 아니기 때문에 fps가 요동칠 때마다
 * 정확한 이동거리를 보장해야한다. 이럴 때에 deltaTime이 그 책임을 담당한다.
 */
import { typeCheckAndClamp } from "/src/engine/utils.js";

/**
 * 이 객체는 이전 프레임과 현재 프레임의 시간차인
 * deltaTime을 계산하는 일을 담당한다.
 */
class Timer {
  constructor() {
    /**
     * 현재 프레임의 시간을 말한다.
     * 웹페이지가 열린 후 지난 시간이 저장된다.
     *
     * @type {number}
     */
    this.currentTime = this.getCurrentTime();
    /**
     * 이전 프레임의 시간을 말한다.
     *
     * @type {number}
     */
    this.previousTime = this.currentTime;
    /**
     * 현재 프레임의 시간과 이전 프레임의 시간의 차를 말한다.
     *
     * @type {number}
     */
    this.deltaTime = 0;
    /**
     * 매 프레임마다 누적된 deltaTime을 저장한다.
     *
     * @type {number}
     */
    this.accumulatedTime = 0;
    /**
     * 1초에 보여줄 프레임의 개수를 말한다.
     * 기본값으로 60이고, 24부터 MAX_VALUE 사이의 값을 저장할 수 있다.
     *
     * @type {number}
     */
    this.fps = 24;
    this.setFps(60);
    /**
     * 물리엔진에서는 가속도를 적분하여 속도를 나타내고,
     * 속도를 적분하여 이동거리를 나타내기 때문에
     * 정확한 연산을 위해서는 수식에서 사용할 ∇t가 일정해야한다.
     * 따라서 fixedDeltaTime은 이론적으로 1 프레임을 렌더링할 때
     * 걸리는 시간을 ∇t로 정한다.
     *
     * @type {number}
     */
    this.fixedDeltaTime = 0;
    this.setFixedDeltaTime();
  }

  /**
   * 현재 프레임과 이전 프레임간의 시간차를 구해 deltaTime에 저장하고,
   * 그 값을 accumulatedTime에 누적한다.
   *
   * 만약 accumulatedTime이 일정값보다 크다면 강제로 조정한다.
   * 이걸 조정하지 않으면 누적된 시간만큼 물리엔진을 업데이트하기 때문에
   * 긴 딜레이가 걸릴 수 있으므로 일부러 값을 낮춘다.
   */
  update() {
    this.currentTime = this.getCurrentTime();
    this.deltaTime = this.currentTime - this.previousTime;
    // 빠른 물리효과 업데이트를 위해 10을 곱하기로 했다.
    this.accumulatedTime += this.deltaTime * 10;
    if (this.accumulatedTime > 0.3) {
      this.accumulatedTime = 0.3;
    }
    this.previousTime = this.currentTime;
  }

  /**
   * 브라우저가 열린 후 또는 새로고침된 후 지난 시간을 반환한다.
   *
   * @returns {number} 초 단위의 지난 시간
   */
  getCurrentTime() {
    return performance.now() / 1000;
  }

  /**
   * 1초에 렌더링할 프레임의 개수를 설정한다.
   *
   * @param {number} fps - 프레임의 개수
   */
  setFps(fps) {
    this.fps = typeCheckAndClamp(fps, "number", 60, 24, Number.MAX_VALUE);
    this.setFixedDeltaTime();
  }

  /**
   * 물리엔진을 업데이트하기위한 fixedDeltaTime을 설정한다.
   * 이 때 Timer의 fps속성을 이용하여 계산한다.
   */
  setFixedDeltaTime() {
    this.fixedDeltaTime = 1 / this.fps;
  }
}

export default Timer;