利用陀螺仪和加速度计构建一个羽毛球游戏
用球拍挥动电脑
我喜欢羽毛球,但夏天太热了,我总是懒得出门。于是我想,为什么不自己制作一个像任天堂游戏里那样的羽毛球游戏呢?幸运的是,我的Cardputer内置了陀螺仪和加速度计,所以我完成了这个项目。
开始
打开电源后,小屏幕会交替显示两个二维码。第一个二维码将你的手机连接到热点(安静地连接——手机保持蜂窝网络),第二个二维码则在浏览器中打开游戏地址192.168.4.1。从口袋出发到参加拉力赛大约需要半分钟。
然后你挥拍。上旋击球是清晰的,下旋则为重击,轻触使球落在网前稍高处,而扫击的方向则决定球向左或向右移动。当球落点周围形成一个环形区域时,挥拍结束的瞬间就是最佳位置。一旦完成接发球,设备屏幕上的配对二维码会切换为一个小比分板——得分、发球标记、比赛状态——这种“这东西就是控制台”的感觉,比我预期的还要强烈。
为什么要在设备上运行所有内容
这个项目早期的版本做了显而易见的事:通过蓝牙(BLE)传输原始IMU数据,然后由浏览器来处理所有计算。虽然这样可行,但Cardputer被简化为一个传感器适配器,而且延迟预算被错误地分配——每次动作都必须经过BLE数据分批处理,游戏才可能判断是否命中。
将游戏迁移到设备后,问题得到了妥善解决。检测现在在与IMU采样相同的芯片上以200Hz运行,因此判断路径完全无需使用无线电。浏览器的任务缩减为仅负责渲染:
有趣的问题是,如何将30Hz的快照流转换为平滑的60fps画面。答案是将标准多人游戏技巧应用于嵌入式系统:每个快照都包含小飞机的位置、速度和设备时间戳,浏览器在绘制前将相同的物理模拟向前推进到“现在”时刻。双方从相同状态运行相同的确定性积分器,因此下一个快照落在预测位置的几像素范围内,修正效果几乎不可见。命中时轨迹会发生变化,这就是为什么发射射击的计时器会立即广播,而不是等待网格刷新。
需要再调整一个时间点。闭合环告诉你要何时摆动,因此必须与设备用来判断你的时钟保持一致。该页面通过WebSocket的ping/pong来估算设备时钟,保留最低RTT的采样数据,并根据设备时间渲染环形图。在局域网中,2–10毫秒的往返延迟下,精度可精确到少于一帧的时间。
摆动发动机
我先尝试了按用户手势训练的方法:录制样本,训练一个小分类器。结果完全无法使用——延迟超过半秒,且频繁误触发。经过深入研究Wii Sports的实际工作原理后,我得出结论:这个问题根本不需要机器学习。一个基于摆动上升沿的阈值状态机效果更好,无需训练和校准:
一些细节很重要。重力只有在设备仍在运动时才会被更新,而在摆动过程中则会被冻结,因为当设备加速时,基于加速度计的姿态校正会明显出错。垂直意图来自固定式集成:从启动时刻起仅使用陀螺仪传递姿态,将加速度计采样数据重新转换回该坐标系,并整合垂直手部速度。这个量实际上就是“手在向上还是向下移动”,与握持方式无关。加速度门限用于排除误报——将设备翻转在手中会产生大量陀螺信号,但产生的向心加速度仅有约0.15g;而即使是最轻微的真正摆动,也会使设备产生0.34g或更高的加速度。
固件引擎是JavaScript原版的C++移植版本,测试套件在81个真实记录的摆动中(包括起火时间、功率和方向,无任何偏差)保持完全一致的输出结果,同时还包含一些合成测试案例(如传感器噪声、缓慢翻转、行走等),这些情况必须始终不触发。
羽毛球物理
羽毛球的终端速度约为6.8米/秒,具有平方型阻力,因此击球时会先在后线前停止,而空心球则几乎垂直下落。
整个飞行模型集于一身的功能之中:
射击动作是基于其上通过二分法构建的。平射用于在固定速度下求解高度,弧形射击用于在固定高度下求解速度,而净滑降则用于寻找最低高度,使得轨迹仍能以一定余量越过胶带。CPU的非强制性错误仅来自难度等级对应的骰子投掷:每发CPU射击在发射前都会被验证是否能成功越过网,若未能成功,则会升级为陡峭的救援提升,用于从胶带正下方挖掘出的穿梭车。"不可战胜"级别实际上从未真正击败自己。
出错的事情
被劫持的门户持续杀死自己的WiFi。第一个网络设计使用了通配符DNS,将所有流量重定向,导致手机弹出“被劫持WiFi”界面,并在其中嵌入游戏。这个设计演示效果很好,但实际体验却糟糕至极:该界面是一个功能受限的迷你浏览器,而在iOS设备上关闭它时,通常会同时断开WiFi连接。解决方法是彻底删除DNS服务器。由于没有DNS,操作系统连接探测功能就会失败,手机会将热点文件归类为“本地网络,无互联网”,静默加入并始终使用蜂窝网络处理其余事务。游戏页面通过第二个二维码中的原始IP地址访问。
每次命中都会导致100毫秒的卡顿。网络清除求解器每发一枪大约会运行上百次轨迹模拟,而它在接触的瞬间——也就是你眼睛所朝向的位置——正好处于60Hz游戏帧率内执行这些操作。三个修改解决了问题:使用-O2而非-Os编译求解器,缩短二分查找深度(反正剩余精度远低于每次射击带来的瞄准噪声),以及提前在CPU“反应”时刻而非接触时刻预计算其返回值。最后这一点令人满意:飞行中轨迹保持不变,因此浏览器的测距推算直接穿过求解器的停滞区域,无人察觉任何异常。
M5Unified 能够静默地将你的冲击力减半。BMI270 在 ±8g 时工作,库文件不会触碰范围寄存器,因此真正的冲击力会被剪切。固件本身设定为 200Hz/±16g,并补偿了驱动程序硬编码的 8g 转换比例。
建造它
这就是整个部署方式:通过 PlatformIO 的预脚本,将网页前端(压缩后约 250KB)嵌入到固件镜像中,因此无需单独的文件系统镜像或二次上传步骤。
测试套件在没有任何硬件的情况下运行:
两名玩家
主机暴露了一个开放的球拍端点:任何将文档中规定的二进制IMU格式流式传输到 ws://192.168.4.1/p2 的设备,都会被识别为第二名球员,屏幕也会随之分割。连接由主机控制——设备会将新加入者作为候选人登记,然后由第一方玩家点击确认接受。固件/cardminton-racket/ 通过约200行代码,将第二个Cardputer转换为该球拍:连接AP、传输IMU数据,并在屏幕上显示候选者/GO状态。一个诚实的说明是:我只有一台Cardputer,因此这个固件虽然能编译并遵循协议,但从未在真实硬件上与主机实际对接过。如果你有两台设备,我很乐意了解使用体验如何。
仓库中包含什么
所有内容均采用MIT许可证:固件、浏览器渲染器、原生测试套件、录制的摆动测试用例,以及包含完整分析过程的设计文档——包括那些未能通过的测试方法。
本文编译自hackster.io





