见缝插针小游戏
把针射入旋转的圆盘,避免针针相撞,挑战 10 关速度极限
什么是见缝插针小游戏?
见缝插针是一款节奏简单但越往后越紧张的休闲小游戏:屏幕中央是一个匀速旋转的白色圆盘,你需要从下方依次把针发射出去,让针稳稳钉在圆盘上,并且不能撞到已经钉住的针。每关给定固定数量的针,全部成功钉入即过关,任何一次相撞都立刻失败。它的原型是手机端非常出名的 AA、Pin Out 这类小游戏,玩法极简,看似随便点点就能玩,真正上手后却很考验时机判断和心理控制。本工具把它搬到浏览器里,无需注册、不要下载,电脑用鼠标点击,手机用手指轻点屏幕即可发射。
使用方法
操作步骤
- 页面打开后游戏会自动进入第 1 关,圆盘开始匀速旋转
- 点击游戏区域(或按空格 / 回车键)发射一根针
- 针朝中心飞行并钉入圆盘,需要确保不撞到圆盘上已有的针
- 把本关所有针全部安全钉入即过关,自动进入下一关
- 任何一次相撞都判定失败,可以选择「重玩本关」或「重新开始」(注意:失败后按空格 / 回车会触发「重新开始」回到第 1 关,想重玩本关需要点按钮)
通关小窍门
- 圆盘是匀速旋转的,找节奏比靠手速更重要:盯着空隙的中线再点。
- 看到旁边针刚刚扫过的瞬间发射,针飞行需要约 120ms,给点提前量。
- 可以连续点击连发,最多缓冲 3 发依次飞出,超出会被丢弃,所以前一发未落地前不必狂点。
- 越往后剩余空位越窄、转速越快,宁可少一次试探也别贪点。
关卡难度说明
- 第 1-3 关:针多、空隙大,主要熟悉操作节奏。
- 第 4-7 关:圆盘已有针变多,需要预判转速。
- 第 8-10 关:圆盘几乎被插满,每根针都得算准时机。
使用场景
技术原理
游戏的主循环由 requestAnimationFrame 驱动,每帧把内部 rotation 增加一个固定的 speed 值(单位:度/帧),然后遍历所有已钉入的针元素,把 transform 设为 rotate(baseAngle + rotation)。因为 transform 是合成层属性,浏览器可以把旋转交给 GPU 合成线程处理,不会触发布局或重绘,所以即使最后一关同时有 15 根针在旋转也能稳定 60fps。 命中检测采用「角度差转弦长」的方式:每根针在圆盘坐标系下用它的 baseAngle 表示(相对圆盘的固定位置,不随旋转改变)。新针抵达中心时,它在世界坐标系下的角度是 TARGET_ANGLE = 0,对应到圆盘坐标系下就是 normalizeAngle(0 - rotation)。然后比较新 baseAngle 和所有已有 baseAngle 的角度差 Δθ,转换为针顶端圆点中心间的弦长 2R·sin(Δθ/2)(R = 111px 是顶端圆点中心到圆心的距离)。如果小于 24px 阈值就算撞针,立即结束本局。 发射动作做了简单的状态机控制:shootLocked 标记一根针在飞行中,防止同一根针的 transitionend 和兜底 setTimeout 触发两次结算;针的飞行时间由 CSS transition 控制为 120ms,180ms 的 setTimeout 兜底是为了在极少数情况下 transitionend 没有触发(例如 DOM 在过渡完成前被改动)时仍能完成结算。pendingShots 缓冲玩家在飞行中连续点击的发射请求,上限为 3 发,超过 3 发的点击直接丢弃,这样既能保留快速连射体验,也不会让多根针同时挤向中心。失败和过关都把 gameOver 置为 true,主循环虽然继续运转,但不再更新 rotation,相当于让画面定格。 为了适配不同分辨率,游戏整体保持 420×720 的设计尺寸,外层根据视口宽高计算缩放比例,套在容器上用 transform: scale。当前实现把上限锁在 1,所以小屏会等比缩小、桌面端大屏不会进一步放大;命中检测用的全部都是设计坐标系下的数字,不需要为每个屏幕重新计算阈值,逻辑简单且任何屏幕上判定都一致。
- 主循环:requestAnimationFrame,每帧 rotation += speed,所有针元素 transform: rotate(baseAngle + rotation)。
- GPU 加速:旋转使用 transform 而非 left/top,浏览器合成层处理,不触发布局/重绘。
- 命中检测:把世界坐标的 0° 反推回圆盘坐标 normalizeAngle(0 - rotation),再用弦长公式 2R·sin(Δθ/2) 与 24px 阈值比较。
- 状态机:shootLocked 防止重复结算,pendingShots 上限 3 发并截断超出(不是排队),gameOver 后冻结 rotation 更新。
- 结算回调:CSS 飞行 120ms,transitionend 主结算 + 180ms setTimeout 兜底,应对 transitionend 偶发不触发的情况。
- 适配方案:固定 420×720 设计坐标系,外层 transform: scale 按视口缩放(上限锁定 1,桌面端大屏保持原始尺寸)。
- 数据保存:仅在 localStorage 中记录历史最佳关卡,不上传任何数据,没有账号体系。
示例
10 关速度与针数配置
关卡 起始针数 本关发射 转速(度/帧)
1 2 8 1.35
2 3 9 1.50
3 3 10 1.65
4 4 11 1.80
5 4 12 2.00
6 5 13 2.20
7 5 14 2.45
8 6 15 2.70
9 7 15 3.00
10 8 15 3.35典型一局:第 5 关失败
第 5 关:初始 4 根针,需要发射 12 根。
第 1-7 发:依次插入空隙,剩余 5 根。
第 8 发:你在前一根落地前抢点,发射时机太早。
判定:新针 baseAngle 与第 6 发只差 8°,小于 DOT_HIT_DISTANCE。
结果:相撞,本局结束,提示「游戏失败」。
可以点「重玩本关」从第 5 关重新开始。全部通关结算
通关耗时:约 2 分 40 秒
累计发射:117 根
失败次数:3
历史最佳:第 10 关
提示:「全部通关!」按钮显示「再玩一次」。
历史最佳写入 localStorage,刷新页面仍然保留。常见问题
为什么我明明点中了空位还是失败了?
因为针不是瞬间到位的,从你点击到针真正接触圆盘大约有 120ms 的飞行时间。这段时间圆盘还在转,如果点击时空隙正好够大,但飞行中圆盘转过去把另一根针带到了正前方,新针落下时就会和它撞上。简单办法是在空隙刚扫过中线时就发射,预留出转动时间。
可以一直按住屏幕连发吗?
可以连续点击,但发射节流是确定的:上一发针没落地前不会发射下一发。在飞行期间最多缓冲 3 次点击依次飞出,超过 3 次的点击会被直接丢弃,不会排队。后期关卡建议放慢点击节奏,等针落地再判断下一发。
游戏会越来越快是按什么规则?
每一关都有独立配置的转速、起始针数和本关需要发射的针数。从第 1 关的 1.35 度/帧到第 10 关的 3.35 度/帧,转速接近 2.5 倍。同时起始针数也从 2 涨到 8,留给你的空隙越来越小,所以越到后面越需要稳,而不只是手快。
失败后是从头来还是从本关来?
两种都支持,但要分清触发方式。点「重玩本关」按钮会从你失败的那一关重开,针数和转速保持不变;点「重新开始」按钮、或在失败画面下按空格 / 回车键,都会回到第 1 关从头打。键盘快捷键不会触发「重玩本关」,习惯按空格继续的玩家要留意。历史最佳关卡始终保留在 localStorage 中。
为什么游戏区域显得比较窄?
游戏的设计尺寸是 420×720,仿照手机竖屏比例。这样在手机上是天然全屏;在桌面浏览器中外层会按视口大小等比缩放,但缩放上限锁定在 1,所以大屏不会进一步放大,看起来就维持在原始 420×720。如果你想要更大的画面,目前只能保持原始比例;游戏不会因为窗口大小改变判定规则。
游戏数据会上传到服务器吗?
不会。整个游戏完全在浏览器本地运行,唯一保存的是「历史最佳关卡」这一个数字,存在 localStorage 里。清除浏览器站点数据或换浏览器就会丢失。我们没有排行榜,也不收集任何分数或操作记录。
手机上玩有没有什么额外注意?
建议在浏览器中开启全屏模式,避免下拉手势误触刷新。横屏其实也能玩,但竖屏比例更接近原版手感。低端机如果出现轻微掉帧,可以试着关闭其他后台标签或减小浏览器窗口里的其他动画元素,本游戏单帧只在做几个 transform 计算,硬件占用极低。