前段时间,我偶然发现 GitHub 上很久以前 star 过的一个手势识别库又有了更新。都已经忘掉了为什么会给这个项目star了,可能是当初刚刚听说手势识别感觉很酷就关注了吧,其实从来也没看过,甚至没有了解过相关代码。
但是这一次偶然的注意,让我想到,这么多年过去了,ai都发展成现在这样了,手势识别一定也很厉害了吧,于是前去了解相关的消息,看到了Google 的 MediaPipe,又很快联想到小时候玩xbox 360的体感游戏,想着如果可以用这个来做一个游戏一定不错吧。
就是这么一个念头在睡觉前总会想起,然后一个类似《Beat Saber》的音乐游戏的概念如圣诞老人般,悄然从我脑中的烟囱中钻出,留下了这么一个礼物:面对笔记本的摄像头,游戏里的方块沿着轨道靠近判定网格,再用手指把它们击中。操作简单易懂,而且貌似也比较好实现。
于是我便利用ai,在做题空闲时描述下需求,一共五六次对话,就做出来了初版。(AI大人的实力真实愈发恐怖了。)
项目代码已经放在 GitHub:MarkXuJQ/finger-block 中。这篇文章记录下我是如何确定游戏的基础设定,以及选用了那些技术选型,和一点点优化的操作。
项目的基础技术结构
- FingerBlock 是一个 Vite + React 的浏览器端项目,因为这个流程我也比较熟悉了。
- 渲染部分使用 React Three Fiber 和 Three.js,用这个来模仿我们的beatsaber的一点点空间感。
- 界面图标使用 Lucide,简单高效。
接下来就该说说游戏的核心内容了:
手势识别:MediaPipe
手势识别使用了 Google 的 MediaPipe Tasks Vision 中的 Hand Landmarker。它提供了手部关键点检测能力,我们利用这个把识别结果转换成 FingerBlock 所需的输入。
useHandTracking.ts中的流程大致是:
- 摄像头启动后,按需加载 MediaPipe 的 WASM 文件和手部模型。
- 优先尝试 GPU delegate,失败时回退到 CPU delegate。
- 限制最多追踪两只手,并读取每只手的食指尖关键点,也就是第 8 个 landmark。
- 摄像头画面给玩家看到的是镜像画面,所以代码会对左右手标签进行交换,保持物理左右和画面左右的一致性。
- 通过
videoLandmarkToViewport把视频坐标转换为视口坐标,再由getLaneFromPoint映射到 >3×3 或 4×4 判定网格。- 对坐标做简单平滑,减少摄像头逐帧检测带来的抖动。
这时,我想既然有了摄像头用手势进行输入的方法, 不如把触控的方式也做了,这样在手机端也可以玩了,于是我也让AI补齐了这个功能,但值得一提的是,在手机和平板端,常常会由于性能不是特别充足,有较大的延迟,或许这个部分可以后面逐渐优化。下面是具体的思路:
摄像头并不是唯一的输入方式。项目后来加入了
pointer模式,鼠标和触控都可以直接在判定区域操作。触控使用pointerId和 pointer capture 追踪多个手指,即使没有触发新的pointermove,也会通过持续的 presence loop 维持 Hold。这样即使浏览器已经取得摄像头权限,玩家仍然可以切换到触控完成一局。
音频分析:把音频处理放到 Worker
从固定谱子转向自动生成
在完成手势识别的功能,我让AI先完成了一段固定曲谱的小样,尝试我的想法是否可行,结果很不错,可以很好的捕捉我的食指位置,识别的也还算准确。
固定谱子跑通之后,内容制作的问题就出现了。
我没有任何音乐版权,也没有能力和时间为大量歌曲手工制作谱子。如果想要像其他音游一样,需要人工标记时间点、位置和方块类型,游戏的扩展成本会很高。但又如果不放音乐,只用固定的曲谱来玩的话,也未免太无聊了些。

这时我想到了 Steam 游戏《Dead as Disco》。我虽然从来没有上手玩过,但是看到过这个游戏的游戏视频。这个让我意识到,我们或许可以和这个游戏一样,尝试做一套音乐分析,然后把根据分析结果来自动生成曲谱,让玩家自己上传自己喜欢的音乐,岂不美哉?
具体实现
音乐导入支持浏览器能够解码的 MP3、WAV、M4A/MP4 和 OGG,单个文件限制为 80 MB,时长限制为 12 分钟,这应该足以应付绝大部分的音乐了,不然一个人玩12分钟的音游也够累的。随后通过 Web Audio API 的 decodeAudioData() 在当前浏览器中解码。
analysis-core.ts的处理过程比简单读取 BPM 更完整:
- 先把多声道混合成单声道,并统一重采样到 22,050 Hz;
- 以大约 50 毫秒为一帧计算整体 RMS 能量;
- 使用 180 Hz 的低通结果估计低频能量;
- 按时间窗口生成归一化能量曲线和波形预览;
- 根据当前帧与前几帧的能量差寻找瞬态,也就是可能对应鼓点或明显起音的时间点;
- 使用
music-tempo的 Beatroot 算法分析 BPM 和节拍候选;- 将歌曲切分为 quiet、normal 和 peak 三种能量乐段;
- 根据节拍匹配率和瞬态覆盖率计算一个置信度。
我使用的 music-tempo 是 MIT 许可证的轻量 BPM/节拍分析库。它适合在浏览器 Worker 中快速给出候选结果,但它不可能对所有弱节奏、自由速度或长时间静音的音乐都判断正确,所以界面保留了人工调整 BPM、首拍和 0.5×/1×/2× 倍速的能力。当然在绝大部分下还是可以卡上大部分的节奏的。
自动谱面生成:规则约束
chart-generator.ts 使用固定种子的伪随机数生成器。种子由歌曲标识、网格大小、难度、BPM 和首拍共同决定,因此同一首音乐在同一组设置下会稳定得到同一份谱面,调整参数后才会得到新的结果。或许这样可以保证一定的稳定性。
生成器会从音频分析结果里读取节拍、能量和瞬态,再根据 easy、normal、hard 三种配置决定采样步长、密度、和弦概率、Hold 概率和最大移动距离。
方块的颜色也和音乐特征相关:重拍或强瞬态生成红色的 accent 方块,普通方块是蓝色,部分非整拍的低强度事件会被标成黄色的可选 bonus。这样来保证游戏的一定的可玩性,同时也利用视觉增强了音乐的节奏性。
不过,真正麻烦的地方在于“随机内容必须仍然可以被双手完成”。人最多就两个食指,天花乱坠的随机方块肯定不能让人类完成,所以我又在项目里加入了几类约束。
同一时间最多占用两根手指
首先把双手食指视为同时可用的最大输入数,生成器用 MAX_SIMULTANEOUS_NOTES = 2 约束重叠判定窗口内的方块数量。即使两个方块的时间戳不完全相同,只要它们处于同一个击打窗口,也会被视为在竞争同样的两根手指。
在高能量的重拍上可以生成双击,但也会检查附近是否已经有足够的方块,避免意外生成第三个同时需要处理的目标。
中间格不放 Hold
从远处接近的 3D 轨道会让中央区域承担重要的视觉参照。如果长时间方块出现在中间,它可能遮住深度轮廓,也会干扰玩家观察后续方块。因此 lane-rules.ts 先定义了 3×3 的一个中心格和 4×4 的四个中心格,beatmap.ts 在校验时直接拒绝位于这些格子的 Hold。
这条规则同时存在于生成阶段和最终谱面校验阶段:生成器使用 excludeCenter 避开中心,校验器则防止未来手工或外部谱面绕过生成器写入非法 Hold。这样规则就不只是一个“希望遵守”的约定,而是变成了数据层的约束。
Hold 会限制另一只手
Hold 不是一个只在起点判定一次的长方块。命中起点后,玩家需要让同一只手持续占据对应格子,项目每 0.25 秒结算一次持续分数,并允许约 0.18 秒的短暂追踪宽限。如果中途松开,已经获得的分数会保留,不会额外再记一次 Miss。
因为一只手被 Hold 占用,另一只手的活动空间就变小了。生成器因此会:
- 只在持续时间足够长、能量不低的非安静乐段中生成 Hold;
- Hold 期间把自由手的生成密度降到普通状态的一部分;
- 让自由手尽量在整拍出现,并预留更长的节拍间隔;
- 限制自由手的移动距离,并优先选择 Hold 所在区域的另一侧;
- Hold 结束后留出恢复空档,再逐渐恢复移动距离和密度。
普通难度的 Hold 持续时间约为两个节拍,简单难度更长,困难难度更短但密度更高。双 Hold 只在足够长的旋律乐段中低概率出现,而且会放在网格的相对两侧,让左右手各自有清楚的目标。laneBlockedUntil 还会阻止同一格在 Hold 结束前生成新的音符。
最终还要校验谱面
所有生成的内容都会经过 validateBeatmap():检查版本、网格大小、难度、歌曲信息、音符排序、音符 ID、颜色与角色是否匹配、Hold 时长是否有效,以及同一格上的音符是否重叠。生成器负责创造内容,校验器负责确保内容符合数据契约,这让后续修改规则时更容易发现问题。
游戏音效:用少量素材组成一套鼓组
在AI时代,逻辑实现变得容易了,但是素材仍然是一个难题,好在网络上有着大量的cc0的开源素材可以使用。由于大部分的音乐都可以垫一层底鼓(不能垫鼓的音乐也不适合玩音游😄),所以很自然的想到使用架子鼓的声音来做打击音效,所有用到的音源都在public/assets/ATTRIBUTIONS.md 中记录了来源、许可证、获取时间、处理方式和 SHA-256 校验值。
当前的击打音效来自 Freesound 上显示为 CC0 的五条录音,包含 KEVOY 的 side stick 和 rimshot、TheEndOfACycle 的 closed hat 和 sub kick,以及 quatricise 的 shaker。项目使用 FFmpeg 对它们做了裁切、淡出、峰值限制、采样率和 Vorbis OGG 转换;
miss.ogg则是低通、低增益处理后的低音闷击。运行时并不是把六个 OGG 当成六种互不相关的提示音,而是按鼓组分层:
- 普通 Hit 播放 side stick;
- Good 在其上叠加轻微 rim;
- Perfect 再加入 closed hat;
- Accent 额外叠加低音 kick;
- Bonus 叠加 shaker;
- Miss 使用单独的低音闷击。
这种设计让不同判定之间有共同的音色锚点,又能通过层数和音色变化区分反馈。音乐通道和效果音通道也分别经过 AudioContext 的 GainNode 控制,避免命中音效覆盖用户导入的音乐。
最后的微调和简单设计
核心逻辑跑通之后,剩下的工作主要是不断试玩和微调:方块的移动速度、轨道的深度感、判定网格的位置、摄像头游标的平滑程度、命中时的闪光和碎片、HUD 上的分数和连击、不同难度的密度,以及音乐和效果音的相对音量。
时钟、判定和反馈
游戏运行时没有把普通的 setInterval 当作唯一时钟。音乐播放使用 Web Audio 的 AudioContext 和 AudioBufferSourceNode,MusicClock 记录音频上下文中的开始时间和暂停位置,游戏逻辑再根据这条音乐时间轴计算方块位置和判定。
普通方块的判定窗口是 ±0.26 秒,其中 ±0.07 秒为 Perfect,±0.15 秒内为 Good,其余仍在总窗口内则为 Hit。普通命中分值分别是 1000、650 和 350,红色 Accent 方块还有 1.2 倍基础分加成。黄色 Bonus 不完全等同于普通漏击目标,它承担的是可选奖励的角色。
Hold 的持续分数也被写成了确定性的函数:每 0.25 秒只消费完整的计分区间,并把时间截断在 Hold 结束处。即使浏览器某一帧延迟了几百毫秒,也不会因为动画帧数量不同而得到不同的持续分数。
命中反馈由 judgement-events.ts 统一生成,Three.js 场景负责播放闪光、扩散环、线框和碎片效果,来增强打击感;结算页还会绘制累计准确率曲线和 Miss 时间点。项目也会在页面隐藏时冻结音乐时钟和 Hold 的补偿逻辑,避免切到后台后回来,方块因为页面暂停而整体跳过判定区域。
当然这一部分完成的很粗糙,因为我感觉大体上已经实现了我的想法了,后续如果没有特殊情况应该也不会再继续优化了。
最后的感想
一天半完成一个可以玩的 Demo
从看到手势识别库更新,到最后完成一个可以运行的 FingerBlock Demo,整个过程大约用了一天半。而且这一天半也不是一直坐在电脑前写代码,中间还花了不少时间做题、看课,中午午休时间用来找找开源库、找找素材、研究音频分析方法,以及不断试玩和调整生成规则。
AI 确实让这个过程变快了很多。搭建初始界面、实现固定谱子、补充交互逻辑、修改生成规则,并处理了不少重复性的代码工作。以前一个想法从零开始到能够运行,可能需要更长时间;现在借助 AI,先做出一个能玩的版本变得容易了很多。
AI 很强,但这次快速实验不全靠AI
我不会把这次快速开发 FingerBlock 的demo完全归功于 AI。
正如我之前写过的一篇当开源只剩下一段 Prompt:我发现了一种“离谱”的开源方式,里面提到的一样,几段好的prompt、清晰的技术栈和恰到好处的素材链接,才是发挥ai最大作用,提高效率的关键。
AI 可以承担越来越多具体的执行工作,但我们不能因此把基本的思考和规划也全部交出去。掌握这些基础能力,才能更快判断一个想法是否可行,更准确地寻找合适的开源资源,也才能看懂 AI 生成的代码到底解决了什么问题、又留下了什么问题。或许是从 Web coding 到当下 Vibe coding 还保留不变的技术关键。
9月23日 坏了,感觉现在再做一遍的话,ai貌似可以做到更多,更好,这是我在群里看到的消息,这些游戏真的可以玩。
以下内容皆使用 Claude Opus 5.5 一句话提示词生成 QQ飞车:https://lf3-static.bytednsdoc.com/obj/eden-cn/nulojnulwlo/qqfeiche3d/index.html 穿越火线之运输船 : https://lf3-static.bytednsdoc.com/obj/eden-cn/nulojnulwlo/cf-transport-ship/transport-ship.html 鹈鹕骑自行车: https://lf3-static.bytednsdoc.com/obj/eden-cn/nulojnulwlo/pelican-bike/index.html
9月25日
今天在网上刷视频刷到了这么个网站,感觉和我的思路有共同之处,还蛮惊喜的.不过这个就要求对音乐要有点点乐理基础,当然玩法也更自由。而且确实蛮cool。
视频:
- Author:Mark Xu
- Copyright:All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating otherwise.
