(素材日期:2026-09-02)
做了 10 年前端,猫眼、新东方、完美世界都待过,也自己创过业。最近把手头的四巧板(T 字之谜)做成了上线形态的微信小游戏「T 字挑战」:88 关、看广告解锁答案、计时。本以为最难的会是拼图算法,结果卡在一个不起眼的拖拽钳制 bug 上,差点让我怀疑人生。这篇把排查过程和顺手搭的自测流水线都记下来,既是复盘,也当求职作品集里的一件硬证据。
问题:拖到边界,块反而往反方向飞
用户在真机预览里反馈:拖一块到屏幕边界,它往反方向走。我第一反应是触摸事件坐标被吃了。但微信开发者工具用鼠标模拟触摸,复现不出来;静态自查坐标变换 s2u / u2s / dragOff / clamp 每一步数学都自洽,没有必现硬伤。
排查:用数值复现脚本定位自引用
我没有继续靠眼睛看,而是写了个数值复现脚本,逐字复刻布局公式和钳制函数,把手指坐标从中心一点点往右边界推,观察块的输出。真相一下清楚:
clampPiecePos 初版用目标位置算块的包围盒,导致钳制上界 maxUX = 常量 - 目标x(斜率 -1)。块一旦越界,输出就随目标反向移动,手指向右、块向左,1:1 弹回。这是典型的负反馈自引用:钳制的输入变量和边界用了同一个待钳制变量。
这个 bug 还污染了 loadLevel 的初始摆放,把块挤到角落,所以上一版图案叠一起、三角位置乱也部分源于此。
修复:边界与目标位置解耦
修法很干净,关键是想透边界必须是不变量:
function clampPiecePos(p, x, y) {
const bb = pieceUnitBounds(p, 0, 0) // 固定锚点算相对偏移,与目标无关
const minUX = (safeArea.left + pad - ox) / scale
const maxUX = (safeArea.right - pad - ox) / scale - bb.maxX
return { x: Math.min(Math.max(x, minUX), maxUX), y: /* 同理 */ }
}
边界用固定锚点 (0,0) 算块的相对包围盒偏移,与目标位置彻底解耦,成了不变量。改完在 375x667 和 375x812(刘海)两机型上都做了数值验证:四块正确落 4 个托盘槽、拖动全程跟手、到四个边界钉住不反向不回弹、回拖立即跟随。
顺手搭的无设备自测流水线
这个 bug 让我意识到:小游戏真机预览成本高,DevTools 模拟触摸又失真,每次改布局都真机点不现实。于是写了 tools/test_game.js:
- mock 微信 API(
createCanvas/getSystemInfoSync/onTouchStart等)和 canvas 上下文(Proxy 空操作); require('../game.js')真跑init,复刻布局常量(scale/ox/oy/TRAY_SLOTS)把 unit 坐标换算成屏幕坐标,发模拟触摸事件;- 自动走选中、旋转、翻转、拖动到解、判定,把 88 关全量顺序通关跑一遍。
逻辑回归从此一条 node tools/test_game.js 搞定,不依赖真机。过程中我还修了两个测试脚本自身的 bug(落点用了旋转前后两个内部点导致偏移、直接拿某一关解拖但游戏还停在首关),但那都不是游戏 bug。
两个配套的工程纪律
第一,打包纪律。小游戏运行时不能 require npm 包或本机绝对路径。我原本把 polygon-clipping 的轮廓计算放运行时,打包后报 module '.../node_modules/polygon-clipping.js' is not defined。改法是挪到构建时:tools/build_levels.js 用 polygon-clipping 求 60 关精确并集轮廓,归一化写死进 utils/gv2_levels.js,运行时只 require 相对路径。一句话:外部库只在构建脚本用,产物以数据形式固化。
第二,兼容性兜底。真机还暴露了两个运行时坑:requestAnimationFrame 在小游戏里是全局函数(不在 wx 上),原来 wx.requestAnimationFrame(loop) 直接崩;新版基础库废弃 getSystemInfoSync,得优先 wx.getWindowInfo 再回退。我都用全局优先、wx 兜底、最后 setTimeout 兜底的三段式兼容掉了。
方法论沉淀
- 任何边界、钳制、约束计算,输入变量和边界不能同源,否则会自引用负反馈。
- 能自动测的别靠手点,尤其小游戏这种真机成本高、模拟器还失真的场景,mock 掉平台 API,逻辑层照样能跑端到端。
- 外部依赖只存在于构建脚本,产物数据化,规避打包环境差异。
如果你也在做 canvas 拖拽或小游戏,遇到过越界反向弹之类的怪现象,欢迎在评论区聊。