
你有没有试过只用一根手指在一台普通笔记本的摄像头前悬停把一块虚拟积木稳准地叠到另一块上面一直叠到几十层、上百层这个“DeepSeek实战”系列的第十四期项目我做的就是这样一件事一个完全靠单只手势控制的叠塔游戏。名字叫“单指叠塔”副标题那句“一只手叠到深空里去”既指游戏里越叠越高的塔也指整个交互方式只用一根手指就能完成。这个项目最初的想法特别单纯键盘和鼠标把“玩游戏”这件事锁死在了桌面上我想试试能不能只用一只手、不碰任何物理设备靠摄像头识别手势完成一局完整的游戏。叠塔这种规则极简、反馈极强的小游戏正好是测试手势交互的最佳场地。而在开发过程中DeepSeek承担了三个角色写核心判定逻辑的辅助大脑、游戏中AI解说的内容引擎、以及排查问题时帮我定位代码思路的“结对同事”。这篇文章会把整个项目的来龙去脉、技术选型、实现链路和踩坑过程完整记录下来。如果你对AI辅助开发、摄像头手势识别、或浏览器端交互游戏感兴趣这篇应该能给你一份可以直接上手的参考。1. 为什么是“单指叠塔”从玩法灵感到项目定位1.1 叠塔游戏的小体量与高反馈叠塔这个玩法本质上是一个“时机判断”游戏一个方块在目标区域上方左右往复移动玩家选准时机让它停下方块落下后叠到塔上。如果对齐得好原来的方块和落下的方块重叠部分会保留超出部分会被裁掉如果裁切过多塔面就会越来越窄下落的方块更难对齐最终在某个临界点轰然倒塌。这种机制的最大特点是规则一句话能说完但每一次操作都给你即时反馈。你的每一次判断对错、每一次手抖造成的偏差都会立刻体现在塔的外观上。对做技术验证来说这是再理想不过的场景——你不需要写复杂的游戏逻辑来解释交互效果因为游戏本身已经足够直观。对玩家来说它又能快速制造“再试一次”的冲动这让手势交互的精度问题不至于被掩盖反而会被玩家主动适应和克服。我选它做这期实战项目的另一个原因是它的体量足够小。单指叠塔不需要地图、不需要角色动画、不需要背包系统核心就是一个物理判定加一个渲染循环。小体量意味着我可以把大部分精力花在“如何用一只手控制它”这个关键问题上而不是被其他无关功能拖住。1.2 DeepSeek在这个项目里不是主角但也不是花瓶很多AI项目的问题在于AI的作用是硬贴上去的明明一个状态机就能解决的事非要套一层大模型结果又慢又贵还不可控。这个项目里我刻意让DeepSeek待在最合适的位置上一共三个角色。第一个角色是开发期的辅助编码。叠塔的“下落-碰撞-裁切-缩窄”逻辑虽然不复杂但对状态转换的时序要求很严格。我先用自己的话把逻辑描述给DeepSeek让它生成一份JavaScript版本的核心判定模块然后我再逐段审查、改掉它没考虑到的边界条件。这个过程大概省了我两个小时的框架搭建时间更重要的是它给我的初始代码结构比我自己习惯的写法更清晰——按游戏状态划分函数而不是一个流程函数写到底。第二个角色是游戏内的AI解说。叠塔游戏如果只是闷头叠其实有点单调。我在每次成功叠上一层、塔层数突破某个里程碑、或者塔倒塌时让DeepSeek生成一句话点评。它有时会调侃“这层偏移了11像素还好塔面够宽”有时会在你连续成功时说“手很稳我开始怀疑你练过”这些实时生成的文案让一个原本安静的游戏有了情绪起伏。第三个角色是运行时判断玩家状态的帮助者。确切说是当我需要一个“是否需要提醒玩家把手移出画面”的指令时不依赖硬编码规则而是把当前识别数据塞给DeepSeek做简单判断。这个功能后面会在实测部分详细说。这样定位的核心逻辑是大模型在最需要“生成内容”或“模糊理解”的地方出现而所有要求精确到毫秒的操作全部走传统代码路径。这也是这个项目最想表达的一个观点——AI和传统逻辑不应该是替代关系应该是分工关系。2. 技术选型与整体架构摄像头、识别模型和大模型API怎么串起来2.1 手部识别方案对比做浏览器端手势识别第一件事就是选方案。当前主流有三条路方案精度浏览器端部署难度性能开销依赖设备MediaPipe Hands单目RGB手部21个关键点能稳定识别指尖、指关节低官方提供JS/WASM包中低普通笔记本可实时运行任意普通摄像头TensorFlow.js自训练分类器取决于训练数据识别“手势类型”可以但关键点定位弱中高需要自己做数据集中普通摄像头深度相机如RealSense/Leap Motion很高带深度信息高需要专用硬件和SDK视设备而定专用硬件说实话对大多数个人项目和演示场景MediaPipe Hands是唯一合理的选择。它不需要专门硬件一个几百块的USB摄像头或笔记本自带摄像头就能跑模型经过充分优化单帧推理延迟在普通设备上能做到几十毫秒级别提供的关键点数据足够支撑“手指悬停、指尖位置、手势类别”这些判断。我在这期项目里用的是MediaPipe Hands的JavaScript版本配合它提供的Camera组件来读取视频流。这里有一个很重要的细节MediaPipe Hands只是输出手部关键点它不负责告诉你“用户想不想点击”。点击意图的判断需要你自己在设计层面解决。2.2 渲染与交互层选择渲染层我纠结过是用纯Canvas还是引入Three.js。叠塔游戏本身是2D平面的方块堆叠用Canvas 2D API完全够用而且在绘制裁切后的异形方块时Canvas的路径裁剪和绘图指令比3D方案更直接。不过为了让倒塌瞬间有更好的视觉效果我最终还是用了Three.js做渲染把2D的叠塔逻辑映射到了3D场景中。方块有厚度倒塌时会有轻微旋转和分离视觉冲击力比纯平面版本强很多。代价是代码复杂度上升但考虑到项目要持续迭代这一点复杂度是值得的。摄像头预览放在页面的右上角小窗游戏主场景居中。所有UI元素都是纯前端实现不依赖任何游戏引擎。2.3 DeepSeek接入方式与触发策略DeepSeek的接入走的HTTP API调用模型用deepseek-chat。我封装了一个简单的请求函数传入系统提示词和用户消息开启流式输出stream: true前端逐段解析返回的文本渲染到对话气泡里。这个调用不是所有时候都在跑。我的策略是事件驱动触发——只有三种情况会发请求玩家成功叠上一层且层数为整十、塔倒塌、检测到玩家连续五秒看向摄像头但手不在画面内。后面两种情况比较少见所以实际运行中API调用频率并不高整体成本和延迟都可控。系统提示词我写得比较“戏精”要求AI以“叠塔教练”的口吻说话每句话不超过30个字带一点幽默感偶尔提及当前层数和偏移量。DeepSeek对这类风格化指令的理解能力很强实测它生成的文案质量比我人工写好几百条模板再接龙的方案要灵活得多。关于整体架构用一个简单的数据流说明摄像头视频流进入MediaPipe识别出指尖坐标前端把坐标映射到游戏区域对应的逻辑位置当指尖在“虚拟按键”区域内停留超过阈值时间触发当前方块下落下落过程由游戏逻辑计算碰撞结果和裁切宽度结果同时驱动渲染层更新画面、触发DeepSeek点评、更新状态机。整个链路都是单向流动的调起来比较省心。3. 核心实现链路从摄像头画面到指尖按下3.1 摄像头权限与画面预览第一步浏览器获取摄像头视频流。使用的WebRTC接口是navigator.mediaDevices.getUserMedia相关约束条件如下const stream await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 30 } }, audio: false });这里有两个细节值得注意。一是分辨率不用开太高。MediaPipe Hands处理640x480的彩色画面时关键点抖动已经很小开1080p反而会增加推理耗时得不偿失。我实测720p和480p下的关键点抖动差异几乎没有但推理帧率从30fps掉到了20fps所以最后锁定了640x480。二是画面必须做水平镜像处理。摄像头画面默认是“照镜子”的效果如果不镜像你往左移动手指画面上指尖往右走这种反向映射会让人在一分钟内就产生强烈的不适感。镜像处理很简单渲染摄像头预览的canvas做一次scale(-1,1)或者给video元素加CSS的transform: scaleX(-1)就行。关键是后续手指坐标映射时也要保持同一套坐标系否则镜像做完坐标还是会错位。3.2 手指关键点识别与坐标映射MediaPipe Hands会为每一只手输出21个关键点每个关键点包含x、y、z三个值。x和y是归一化到[0,1]的图像坐标z是以手腕为原点的深度估计值。单指叠塔的核心识别目标是“用户右手食指的指尖位置”。代码如下async function processFrame() { const detections await hands.send({ image: videoElement }); if (detections.multiHandLandmarks detections.multiHandLandmarks.length 0) { const landmarks detections.multiHandLandmarks[0]; const indexTip landmarks[8]; // INDEX_FINGER_TIP const thumbTip landmarks[4]; // 坐标映射 const gameX (1 - indexTip.x) * gameCanvasWidth; const gameY indexTip.y * gameCanvasHeight; // 更新虚拟指针位置 updateVirtualCursor(gameX, gameY); } requestAnimationFrame(processFrame); }坐标映射时有一行很容易写错gameX (1 - indexTip.x) * width。因为画面做了镜像MediaPipe输出的原始x坐标和显示画面是反向的所以必须先对x做1 - x再映射。这里如果忘了镜像你手指明明指向屏幕左边指针却在右边整个操作就全乱套了。还有一点我只取了multiHandLandmarks[0]也就是第一只手。如果两只手同时出现在画面里MediaPipe的排序并不保证稳定可能出现指尖在两只手之间跳变。对这个场景建议当检测到两只手时保持上一帧的单手结果而不是切换到新的识别结果。3.3 虚拟按钮与“按下去”的判定逻辑手势识别最难的不是识别出你在哪而是判断你“想不想触发”。所以我设计了一个虚拟按键在游戏画面底部中央画了一个半透明的圆形区域玩家的食指指尖进入这个区域就开始累积“悬停时间”悬停时间超过阈值视为一次点击。我对比过两种方案最终选了悬停计时方案优点缺点悬停计时指尖进入按钮区域并停留N毫秒稳定误触率低不需要做手势分类响应略慢需要一个明确的按钮区域捏合手势拇指和食指靠近触发感强可任意位置触发需要额外做手势分类容易误判手部遮挡时识别不稳定阈值我一开始设的是800毫秒后来测下来还是偏长玩家在紧张状态下会觉得方块落不下来。最后调成了500毫秒并加了视觉反馈——指尖进入按键区域后按钮会逐渐变色填充填充到100%时触发。这样玩家能直观看到“我快要按下了”而不是干等。判定代码如下let hoverStartTime 0; let cursorInButton false; function updateButtonState(cursorX, cursorY, timestamp) { const isInside checkCircleContains(cursorX, cursorY, buttonPos, buttonRadius); if (isInside !cursorInButton) { cursorInButton true; hoverStartTime timestamp; } else if (!isInside cursorInButton) { cursorInButton false; hoverStartTime 0; } if (cursorInButton timestamp - hoverStartTime HOVER_THRESHOLD_MS) { triggerDropBlock(); cursorInButton false; hoverStartTime 0; } renderButton(cursorInButton, timestamp - hoverStartTime); }这套判定的核心思想是意图必须稳定持续一段可观测的时间才算一次有效操作。500毫秒在游戏操作里不算短但如果把阈值降到200毫秒以下你就会发现任何一点手部抖动都会造成误触那时候玩家的挫败感会远大于“稍微等半秒”的不耐烦。3.4 叠塔游戏状态机叠塔游戏的状态流转可以用一个简单状态机概述MOVE_DOWN当前方块水平往复移动等待玩家触发PRE_DROP_DELAY触发瞬间的短暂停顿让玩家有心理预期FALLING方块垂直下落做碰撞检测MERGING计算相交区域裁切超出部分更新塔面CHECK_OVER判断塔面是否过窄或已到目标层数AI_COMMENT触发DeepSeek点评并显示文本每个状态都有独立的入口和出口requestAnimationFrame的每一帧根据当前状态调用对应的update函数。这个结构是DeepSeek帮我搭的框架实际填充逻辑时我改了两处一是增加了PRE_DROP_DELAY状态因为如果没有这个短暂的停顿玩家会感觉方块在点击瞬间就消失了缺少“按下去”的确认感二是把碰撞检测从逐像素比较改成了矩形相交计算性能更高且逻辑更清晰。矩形相交计算的核心函数function computeOverlap(prevRect, newRect) { const overlapLeft Math.max(prevRect.left, newRect.left); const overlapRight Math.min(prevRect.right, newRect.right); const overlapWidth overlapRight - overlapLeft; if (overlapWidth 0) return null; // 完全错开 const scaleX overlapWidth / newRect.width; const mergedRect { left: overlapLeft, right: overlapRight, width: overlapWidth, bottom: prevRect.bottom newRect.height, top: prevRect.bottom }; // 裁切后剩下的左右两端掉落 const leftCut { left: newRect.left, width: overlapLeft - newRect.left, top: newRect.top, bottom: newRect.bottom }; const rightCut { left: overlapRight, width: newRect.right - overlapRight, top: newRect.top, bottom: newRect.bottom }; return { mergedRect, scaleX, leftCut, rightCut }; }上面的leftCut和rightCut是超出塔面的部分视觉上我会让它们在落地瞬间滑落并消失。倒塌判定发生在overlapWidth小于初始方块宽度的15%时——这个时候塔面已经窄到很难继续叠了直接播倒塌动画比让玩家继续挣扎更痛快。4. 让“放下”有手感灵敏度校准与多重反馈设计4.1 灵敏度校准的现实问题做手势交互项目最容易被忽视但又最影响体验的问题是人跟人之间的差异太大。有人手指悬停很稳有人天生就有点轻微震颤同一个500毫秒阈值前者觉得太慢后者觉得刚好。更麻烦的是环境光线的变化——光线不足时MediaPipe的关键点抖动会明显增大。我的做法是做了一套灵敏度校准流程游戏开始前让玩家把食指悬停在按钮区域10秒钟系统统计这段时间指尖坐标的波动范围把波动半径作为该玩家的“稳定度基准值”。如果基准值小于10像素判定阈值保持500毫秒如果大于20像素则把判定阈值上调到800毫秒同时扩大按钮的碰撞半径。这个校准过程本身也是一次教学玩家在10秒内必须保持手指在按钮里这在无形中教会了玩家“手指要稳定放在这里才能操作”。所以它既是调参工具又是新手引导一举两得。4.2 视觉反馈设计视觉反馈包括三个层次指针层、按键层、方块层。指针层是一个跟随指尖移动的小圆点颜色会随进入按键区域而变化平时淡蓝色进入按钮区域后变黄。这一层的作用是解决一个基本问题——让玩家知道“系统现在认为我的手在哪里”。如果没有这个指针玩家根本不知道自己的手指有没有被识别到会进入一种“我是不是没对准”的恐慌状态。按键层是刚才提到的填充式虚拟按钮。填充速度和悬停时间成正比玩家可以通过填充进度准确预判还剩多久触发。这比“到时间突然触发”友好得多。方块层是指当前正在移动的方块本体。当悬停触发、方块即将下落时我会让方块闪烁一下并轻微放大提示玩家“你的指令已生效”。这个反馈极其关键否则玩家会怀疑是自己的手抖还是系统不响应一旦产生这种怀疑后续操作就会开始慌乱。4.3 AI解说的话术设计DeepSeek的解说功能是这期项目最容易被记住的部分。我设了三个触发节点每成功叠5层生成一句鼓励或点评要求提及当前层数和偏移量塔倒塌时生成一句“总结陈词”检测到玩家手部离开画面超过3秒生成一句引导提示系统提示词我最终的版本是这样的你是一个叠塔游戏现场的AI解说员。你在观看玩家用一只手玩叠塔游戏。 当玩家成功叠上一层时你要点评他的表现。要求 1. 每句话不超过30个字口语化 2. 可以提到当前塔层数和最近一层的偏移像素 3. 偶尔用调侃的语气但不要嘲讽 4. 不要连续两次说类似的话返回的文本直接显示在画面左下角的对话气泡里同时用Web Speech API的speechSynthesis.speak朗读。这一步非常出效果——原本安静的游戏突然有了一个实时点评的声音整个观看体验立刻不一样了。把DeepSeek生成文案这个行为放在这种轻量场景里比让它控制游戏逻辑要靠谱得多因为即使它偶尔生成了一句不那么精准的话也只会增加趣味性不会破坏游戏运行。5. 踩坑记录从“识别很准”到“游戏很难玩”之间发生了什么5.1 识别是准的但它不知道你什么时候想松手项目跑到中后期我一度陷入一个困惑MediaPipe的关键点输出稳定指尖位置也很准但游戏就是不好玩。方块移动速度调慢也不行玩家总是抱怨“我想按的时候按不出来不想按的时候它自己落下去”。这个问题我排查了很久最后发现根因不在识别层而在交互模型本身。我一开始把“虚拟按钮”设计成了一个需要玩家主动把手指移入并停留的区域。但在叠塔游戏里玩家的注意力高度集中在移动方块上他倾向于用“眨眼一样快速点按”的方式来操作而不是有意识地移动手指去一个固定区域再停住。解决办法是把虚拟按键区域从屏幕底部改成覆盖整个塔面区域并调整触发逻辑。当前方块下方画一个高亮落点区域玩家只要把指尖悬停在塔面任何位置并保持稳定就可以触发下落。这样一来玩家的眼睛看方块、手指在塔面上空微调、形成稳定状态后自然触发整个操作链路从“移过去-对准-等待”变成了“对准-稳住-自动落下”更符合直觉。5.2 模型加载白屏时间太长MediaPipe的WASM资源首次加载可能需要几秒到十几秒取决于网络。如果在游戏启动时才加载模型玩家会面对一个完全空白或只有一个loading文字的画面观感很差。解决思路是预加载。用户打开主页时先显示一个“正在初始化手势引擎”的过渡页同时后台加载MediaPipe的模型文件和WASM依赖。等资源全部准备完成后再进入游戏场景。实测首次加载时间从页面上看并没有变短但玩家看到的始终是一个有品牌感、有进度条的页面而不是白屏感知上的等待时间大幅下降。另外MediaPipe支持通过locateFile参数指定模型文件路径。把我用的CDN版本改成自己服务器上的静态文件后网络不稳定时的加载成功率提升了大概20%。这套静态资源的本地化部署建议所有依赖WASM模型的项目都做一次。5.3 摄像头帧率与判定延迟在低配笔记本上MediaPipe Hands的推理耗时可能会达到80-100毫秒每帧加上后续的渲染和游戏逻辑整体操作延迟接近200毫秒。这个数值对叠塔游戏来说是能玩的但如果帧率再降到10fps以下指尖轨迹就会出现明显跳变定位不准导致误触。我做了两个优化一是把MediaPipe的推理帧率限制在20fps而不是每一帧都跑游戏渲染帧率保持独立的60fps指尖位置在两帧推理之间用线性插值补全二是降低摄像头分辨率到480p。两个操作叠加之后整体延迟降到了120毫秒左右游戏体验顺利进入“可以接受”的范围。这里有一个值得分享的经验不要把识别帧率和渲染帧率绑定在同一个requestAnimationFrame里。识别是重计算渲染是轻计算各自按自己的频率跑用最新一次的识别结果去驱动渲染能最大限度减少互相拖累。5.4 AI点评频繁导致的“话痨”问题DeepSeek接入初期我把触发频率设成了每成功叠一层就调用一次。结果就是游戏画面左下角几乎一直在转菊花AI说话比玩家操作还频繁完全淹没了游戏的节奏感。这个问题的本质不是API太慢而是玩法节奏和AI输出频率不匹配。叠塔的节奏是“快速操作-短暂停顿-快速操作”如果在每一次停顿里都塞入一次流式API调用停顿时间会被拉长到让玩家等待的程度。最终的方案是整十层才做AI点评平时只显示简短的系统提示语“stable”或“off-center”。塔倒塌时才追加一次AI总结。这样每局游戏大约只会调用3到5次API延迟不再是问题而且因为点评次数少了玩家反而更期待那句AI的话。6. 实测结果与下一步扩展6.1 不同环境下的实测数据项目基本跑通后我做了一轮多环境实测重点看三组数据测试环境识别帧率操作延迟单局平均层数误触率室内正常灯光桌面笔记本28-30fps约120ms28层3%室内暖黄灯低照度20-24fps约160ms19层8%逆光靠窗位置16-18fps约210ms12层15%结论很明确光线是最大的变量。逆光环境下MediaPipe的关键点抖动明显增加误触率和手感都下降得厉害。实测之后我在游戏启动前的校准环节增加了一项检查——如果模型连续30帧无法稳定识别到食指指尖会提示玩家“环境光线可能不足建议调整位置或增加光源”。手指距离摄像头的远近也很关键。距离太近小于30厘米会导致手部超出取景范围关键点截断距离太远超过80厘米会导致手部在画面里太小关键点定位精度下降。最合适的范围是40到60厘米我在UI上放了一个半透明的“距离指示条”通过计算手部包围盒面积估算距离太近显示红色警示太远显示蓝色提示。6.2 从叠塔延伸出去的其他玩法这个项目的技术栈完全可以在不重写架构的情况下扩展到其他场景。最简单的扩展是把叠塔改成“指尖画画”。把指尖轨迹实时绘制到画布上配合DeepSeek生成对画作的实时点评就是一个“AI看你在白纸上乱画还不停夸你”的娱乐应用。技术难度比叠塔还要低因为不需要精确的点击判定只需要跟踪轨迹。另一条思路是把虚拟按键换成不同的动作语义。比如食指悬停是点击、五指张开是清空、握拳是确认这样一个单摄像头手势识别器就能变成键盘鼠标之外的第二输入设备。对部分不方便使用双手的用户来说这种交互方式有实际意义不只是炫技。再往深一点的方向叠塔游戏本身也可以加规则。比如让方块在下落过程中有轻微旋转或者每隔10层出现一个比当前塔面宽度更宽的“救急方块”这些变种玩法都可以在现有状态机框架里快速实现。核心状态、判定逻辑、AI点评系统都不用动只要扩展MOVE_DOWN阶段的行为就行。我在实际跑完整个项目之后的体会是所谓“一只手叠到深空里去”真正难的不是让方块无限叠高而是让人与机器之间的交互足够自然。当你能用最少的物理动作完成一个完整的游戏闭环这种体验带来的满足感是键盘鼠标无法替代的。下次如果你也想做个摄像头交互的小项目别忘了这个经验先让玩家知道系统看到了什么再让系统理解玩家想干什么最后才轮到让AI替你说话。这三步顺序反了项目大概率会卡在“看起来很酷但玩起来很难受”的中间地带。