TOOLS GAMES DEVLOG DOCS ABOUT CONTACT
HOME > DOCS > Web / Tools > HTML5 Canvas 고정 델타 타임(Fixed Timestep) 루프와 Web Audio 절차적 효과음 합성HTML5 Canvas Fixed Timestep Loop and Procedural Web Audio Synthesis
Web / Tools Intermediate HTML5 Canvas / Web Audio API

HTML5 Canvas 고정 델타 타임(Fixed Timestep) 루프와 Web Audio 절차적 효과음 합성HTML5 Canvas Fixed Timestep Loop and Procedural Web Audio Synthesis

144Hz 이상 고주사율 모니터에서도 물리 연산 속도를 일정하게 유지하는 requestAnimationFrame 누적기 루프와 외부 오디오 파일 없이 Web Audio API로 효과음을 합성하는 기법입니다.Maintaining deterministic physics across 144Hz+ high-refresh-rate monitors with a requestAnimationFrame accumulator loop and synthesizing sound effects via the Web Audio API without external audio files.

#Canvas #WebAudio #GameLoop #FixedTimestep #JavaScript
핵심 개념브라우저의 requestAnimationFrame은 사용자의 모니터 주사율(60Hz, 120Hz, 144Hz, 240Hz)에 맞춰 호출됩니다. 프레임마다 단순 위치 덧셈으로 물리를 갱신하면 고주사율 모니터에서 게임 속도가 2~4배 빨라집니다. 시간 누적기(Accumulator)를 두어 고정 시간 간격($dt = 1/60\text{s}$)으로만 물리 상태를 전진시키고, 사운드는 Web Audio API의 OscillatorNode와 GainNode 주파수 스윕으로 합성하면 외부 에셋 로드 없이 경량 웹 게임을 구현할 수 있습니다.
Key ConceptThe browser's requestAnimationFrame fires at the user's native display refresh rate (60Hz, 120Hz, 144Hz, 240Hz). Updating physics directly per frame causes the game to run 2x to 4x faster on high-refresh monitors. Using a time Accumulator advances physics strictly in fixed increments ($dt = 1/60\text{s}$), while synthesizing sound effects through Web Audio API OscillatorNode and GainNode frequency sweeps eliminates external audio asset dependencies.

1. 고정 타임스텝 메인 루프 및 Web Audio 신디사이저 구현1. Implementing Fixed Timestep Main Loop and Web Audio Synthesizer

performance.now()로 측정한 프레임 간 경과 시간을 accumulator에 누적한 뒤 고정 틱(FIXED_DT) 단위로 물리 루프를 소화하고, 사용자 입력 이벤트 발생 시 오디오 컨텍스트에서 주파수 감쇠 엔벨로프를 스케줄링합니다.

Accumulate elapsed frame time measured via performance.now() and step the physics simulation in fixed FIXED_DT intervals. When user input occurs, schedule frequency and amplitude decay envelopes directly on the Web Audio graph.

canvas_loop_and_synth.js
// --- [1. 고정 델타 타임(Fixed Timestep) 캔버스 루프] ---
const FIXED_DT = 1 / 60; // 60Hz 고정 물리 스텝 (약 16.67ms)
const MAX_FRAME_TIME = 0.25; // 탭 전환 복귀 시 나선형 사망(Spiral of Death) 방지 상한

let lastTime = performance.now();
let accumulator = 0;

function gameLoop(now) {
    let frameTime = (now - lastTime) / 1000;
    lastTime = now;
    if (frameTime > MAX_FRAME_TIME) frameTime = MAX_FRAME_TIME;

    accumulator += frameTime;
    while (accumulator >= FIXED_DT) {
        updatePhysics(FIXED_DT); // 주사율과 무관하게 항상 동일한 dt로 적분
        accumulator -= FIXED_DT;
    }

    const alpha = accumulator / FIXED_DT; // 렌더링 보간 계수 (0.0 ~ 1.0)
    renderCanvas(alpha);
    requestAnimationFrame(gameLoop);
}

// --- [2. Web Audio API 절차적 효과음(점프/타격) 합성] ---
let audioCtx = null;

function playJumpSfx() {
    if (!audioCtx) {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }
    if (audioCtx.state === 'suspended') audioCtx.resume();

    const t0 = audioCtx.currentTime;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();

    // 구형파(Square)로 레트로 아케이드 질감 생성 및 주파수 상향 스윕
    osc.type = 'square';
    osc.frequency.setValueAtTime(180, t0);
    osc.frequency.exponentialRampToValueAtTime(620, t0 + 0.14);

    // 클릭 노이즈 방지를 위한 지수 감쇠 볼륨 엔벨로프
    gain.gain.setValueAtTime(0.18, t0);
    gain.gain.exponentialRampToValueAtTime(0.001, t0 + 0.15);

    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.start(t0);
    osc.stop(t0 + 0.15);
}
canvas_loop_and_synth.js
// --- [1. Fixed Timestep Canvas Loop] ---
const FIXED_DT = 1 / 60; // 60Hz fixed physics step (~16.67ms)
const MAX_FRAME_TIME = 0.25; // Upper bound to prevent Spiral of Death after tab switching

let lastTime = performance.now();
let accumulator = 0;

function gameLoop(now) {
    let frameTime = (now - lastTime) / 1000;
    lastTime = now;
    if (frameTime > MAX_FRAME_TIME) frameTime = MAX_FRAME_TIME;

    accumulator += frameTime;
    while (accumulator >= FIXED_DT) {
        updatePhysics(FIXED_DT); // Integrate with identical dt regardless of refresh rate
        accumulator -= FIXED_DT;
    }

    const alpha = accumulator / FIXED_DT; // Render interpolation factor (0.0 ~ 1.0)
    renderCanvas(alpha);
    requestAnimationFrame(gameLoop);
}

// --- [2. Web Audio API Procedural Sound Effect (Jump/Hit) Synthesis] ---
let audioCtx = null;

function playJumpSfx() {
    if (!audioCtx) {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }
    if (audioCtx.state === 'suspended') audioCtx.resume();

    const t0 = audioCtx.currentTime;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();

    // Square wave for retro arcade timbre with upward frequency sweep
    osc.type = 'square';
    osc.frequency.setValueAtTime(180, t0);
    osc.frequency.exponentialRampToValueAtTime(620, t0 + 0.14);

    // Exponential decay volume envelope to prevent click artifacts
    gain.gain.setValueAtTime(0.18, t0);
    gain.gain.exponentialRampToValueAtTime(0.001, t0 + 0.15);

    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.start(t0);
    osc.stop(t0 + 0.15);
}

2. 핵심 타이밍 및 오디오 파라미터 명세2. Core Timing and Audio Parameter Specification

좌우로 밀어서 표 전체 내용을 확인하세요
파라미터 / API설정값 / 범위역할 및 동작 특성
FIXED_DT1 / 60 (0.01667s)모니터 주사율(60Hz~240Hz)에 상관없이 동일한 점프 포물선과 충돌 결과를 보장
MAX_FRAME_TIME0.25 (250ms)브라우저 백그라운드 탭 이동 후 복귀 시 수백 번의 물리 루프가 한꺼번에 도는 현상 차단
exponentialRampToValueAtTime> 0.0001 (양수 필수)주파수 피치 스윕 및 볼륨 감쇠 곡선 생성 (0을 전달하면 RangeError 예외 발생)
devicePixelRatiowindow.devicePixelRatio || 1레티나/고해상도 디스플레이에서 캔버스 픽셀이 흐려지지 않도록 내부 버퍼 해상도 배율 조정
Swipe horizontally to view the full table
Parameter / APIValue / RangeRole and Behavior
FIXED_DT1 / 60 (0.01667s)Guarantees identical jump arcs and collision outcomes across 60Hz to 240Hz displays
MAX_FRAME_TIME0.25 (250ms)Prevents hundreds of physics steps from executing all at once when returning from a background tab
exponentialRampToValueAtTime> 0.0001 (must be positive)Shapes pitch sweeps and volume decay curves (passing 0 throws a RangeError)
devicePixelRatiowindow.devicePixelRatio || 1Scales internal canvas buffer resolution to keep graphics sharp on Retina/HiDPI screens

3. 실무 적용 및 주의사항3. Practical Considerations and Caveats

  • 브라우저 자동 재생 정책(Autoplay Policy) 준수: 크롬과 사파리는 사용자의 명시적인 클릭(pointerdown)이나 키 입력(keydown) 이전에 생성·재생된 AudioContext를 suspended 상태로 차단하므로, 첫 입력 핸들러 내부에서 audioCtx.resume()을 호출해야 합니다.
  • 지수 감쇠 함수의 0값 전달 금지: GainNode.gain.exponentialRampToValueAtTime(value, endTime)은 수학적으로 로그 스케일을 사용하므로 목표값에 0을 넣으면 오류가 발생합니다. 무음 처리는 0.001 이하의 작은 양수로 감쇠시킨 뒤 osc.stop()으로 노드를 종료해야 합니다.
  • 백그라운드 탭 복귀 방어: requestAnimationFrame은 비활성 탭에서 일시 정지되었다가 탭 활성화 시 수 초의 now - lastTime을 한 번에 전달하므로 MAX_FRAME_TIME 클램핑이 필수입니다.
  • Comply with Browser Autoplay Policies: Chrome and Safari suspend any AudioContext created before an explicit user gesture (pointerdown or keydown), so always call audioCtx.resume() inside the input handler.
  • Never Pass Zero to Exponential Ramps: GainNode.gain.exponentialRampToValueAtTime(value, endTime) uses a logarithmic curve and throws an error if the target value is 0. Ramp down to 0.001 instead and terminate the node with osc.stop().
  • Clamp Delta Time After Tab Switching: requestAnimationFrame pauses in background tabs and delivers a multi-second delta upon resume, making MAX_FRAME_TIME clamping essential.

어큐뮬레이터 기반 고정 델타 타임 루프와 Web Audio 절차적 신디사이저를 결합하면, 네트워크 오디오 에셋 로딩 지연 없이 어떤 주사율의 디바이스에서도 일관된 조작감을 제공하는 웹 게임을 제작할 수 있습니다.

Pairing an accumulator-based fixed timestep loop with procedural Web Audio synthesis delivers consistent gameplay physics and zero-latency sound effects across devices of any refresh rate.