网页节奏游戏的时间判定:AudioContext 时钟、输入缓冲与延迟补偿
浏览器里做一个能玩的节奏游戏,难点通常不在谱面绘制,而在“什么时候算按准了”。如果直接用 Date.now() 或 requestAnimationFrame 的时间戳判断输入,短暂掉帧、音频启动延迟和设备差异都会让判定忽松忽紧。这篇笔记记录一套基于 Web Audio 时钟的轻量实现。
一、让 AudioContext 成为主时钟
视觉动画可以掉帧,但音乐不能跟着跳。比较稳妥的做法是让 AudioContext.currentTime 作为音乐时间轴,把每个音符的目标时间预先算成音频时钟上的秒数。按下按键时,再用同一个时钟计算输入时间:
function songTime() {
return audio.currentTime - startOffset;
}
function scheduleNotes(chart) {
const startAt = audio.currentTime + 0.2;
for (const note of chart.notes) {
const when = startAt + note.beat * secondsPerBeat;
playClick(when);
}
startOffset = startAt;
}
先留出 200 毫秒左右再启动,可以避开首次创建 AudioContext 时的抖动。所有音符和判定都使用这条时间轴,界面动画只负责把秒数映射成屏幕位置。
二、判定窗口要有中心而不是只看绝对值
最直接的实现是计算输入时间与目标时间的差值 delta,再按不同区间给出 Perfect、Good 或 Miss。但输入与音符之间可能重复匹配,所以要给每个音符记录是否已经被命中,并在一个很小的候选集合里寻找最近的目标:
const windows = {
perfect: 0.045,
good: 0.090,
miss: 0.140,
};
function judge(delta) {
const error = Math.abs(delta);
if (error <= windows.perfect) return 'perfect';
if (error <= windows.good) return 'good';
return 'miss';
}
窗口不应该无限放宽。Miss 的边界之外直接忽略输入,可以让游戏保持节奏感,避免玩家乱按也能得到大量判定。
三、输入缓冲解决“同一帧按两次”
浏览器的事件循环并不保证每个 keydown 都会在下一帧单独到达。高速连打时,两次输入可能在同一帧被处理,甚至一次输入会在音频调度前几毫秒发生。我的做法是把输入先写入缓冲区,并记录事件时间:
const inputQueue = [];
window.addEventListener('keydown', (event) => {
if (event.repeat) return;
inputQueue.push({
lane: keyToLane[event.code],
at: songTime(),
});
});
function consumeInputs() {
while (inputQueue.length) {
const input = inputQueue.shift();
matchNearestNote(input);
}
}
真正的匹配要按时间顺序消费,不能因为后一个音符更近就抢走前一个输入。对每个输入只匹配同一轨道、尚未命中、误差在 Miss 窗口内的最近音符即可。
四、播放延迟需要单独补偿
有些设备的系统输出延迟明显,玩家听到声音时才按键,代码记录到的时间会晚几十毫秒。可以提供一个校准入口:让玩家连续跟着节拍按键,取多次误差的中位数作为 inputOffset,判定时先从 delta 中减去它。
const corrected = input.at - target.at - inputOffset;
const result = judge(corrected);
不要用第一次校准结果直接永久保存。设备、耳机和系统音频路由变化时,延迟会变;最好允许在设置页重做校准,并把结果存在 localStorage 中作为默认值。
五、渲染与状态要解耦
音符的屏幕位置只由“当前歌曲时间”和“音符目标时间”决定。即使某一帧卡顿,下一帧也会直接追到正确位置,而不是把音符逐帧累加移动。命中结果则进入独立的计分状态,负责连击、得分和特效,互不修改彼此的时钟。
const y = judgeLineY - (note.at - songTime()) * pixelsPerSecond;
drawNote(note.lane, y);
最后补上暂停、恢复和失焦处理。暂停时要记录音频时钟的相对位置,恢复时重新排程;页面失焦则自动暂停,避免玩家切走后整首谱面错过。这样一套结构不依赖音频素材,只用振荡器生成的短音也能做出完整可玩的网页节奏游戏。
- 点赞
- 收藏
- 关注作者
评论(0)