《萤虫跳跳》是运行在 Rokid 智能眼镜上的 AIUI 跳台游戏。低头调整力度,缓慢回抬可以收力,快速回抬才触发起跳。实现的关键在于把连续变化的力度和只触发一次的起跳事件分开处理。
AIUI ID · 4653d9dab9074f8eb82253642eb62536
版本说明。 文中的游戏截图截自 v1.0.3 实机演示视频,用于展示当时的操作和界面。参数、代码、计分与月榜逻辑均按 1.0.6 源码核对。方案图和流程图用于说明代码逻辑。
1. 从平视校准到落点判定
玩家在首页点头或按镜腿确认键后进入校准,校准通过后倒数三秒开局。首页和结算页接收点头确认,游戏中则根据低头角度和回抬速度处理头部动作,避免蓄力时触发页面确认。
图 1 游戏首页的蓄力与起跳提示。
图 2 头控方案。姿态传感器计算力度,陀螺仪判断起跳时机,游戏用锁定的力度计算跳距。
每局以自然平视为零点,减少眼镜佩戴位置对低头角度的影响。校准预留约 1 秒准备时间,随后需要连续稳定至少 1.5 秒,并收集至少 24 个新姿态样本。姿态变化、角速度或采样间隔超出限制时,重新累计稳定区间。通过后取姿态四元数的平均值作为基准。
AbsoluteOrientationSensor 提供当前姿态。游戏计算相对于基准的低头角度,平滑后换算成度,再将 8° 到 28° 线性映射为 0 到 1 的力度。例如平滑后的低头角为 18°,对应半力。
const deg = depthRadians * RAD_TO_DEG;
return clamp(
(deg - HeadControlConfig.TRIGGER_ANGLE_DEG)
/ (HeadControlConfig.FULL_ANGLE_DEG
- HeadControlConfig.TRIGGER_ANGLE_DEG),
0,
1
);
蓄力时,预测线将力度转换为可见的跳跃距离。如果预测点越过目标台,玩家可以缓慢回抬,减小力度,角色仍停在起跳台上。
图 3a 较低力度下,预测落点位于两座跳台之间。
图 3b 起跳位置不变,力度升高后,预测落点移到目标台中心附近。
快速回抬时,低头角度已经开始变小。为了保留刚蓄好的力,Gyroscope 回调在回抬速度越过约 120°/秒 时检查校准状态、有效蓄力和样本时效。满足条件且没有待处理事件时,pendingLift 记录事件时间和上一游戏帧的有效力度。游戏帧取出后立即清空,丢弃超过 120 毫秒 或姿态样本已过期的事件。这里的 120 毫秒是有效期,事件通过检查就能在当前游戏帧起跳。以下摘录省略了判定条件。
// 陀螺仪回调:越过阈值且满足条件时保存力度
this.pendingLift = { at: this.lastGyroAt, power: this.inputPower };
// 游戏帧:取出事件后清空,只处理一次
const event = this.pendingLift;
this.pendingLift = null; // 一个动作只消费一次;过期动作直接丢弃。
// Ink 页面:起跳用锁定值,蓄力用当前值
this.power = h.launched ? h.launchPower : h.power;
图 4 v1.0.3 录像中的飞行画面。角色从右侧起跳台飞向左侧目标台。
v1.0.3 实机录屏的 00:07–00:09 展示了蓄力、起跳和落地。
图 5 1.0.6 起跳时序。陀螺仪回调读取已保存的力度,游戏帧在取走事件后检查时效。图中间隔仅表示先后。
跳台生成与跳跃共用同一套坐标。 游戏从当前台心向左上或右上生成下一台。以 d 表示台心在 x 轴上的间距,下一台的 x 偏移为 ±d,y 偏移为 −0.556 × d。力度同样换算成 x 轴上的跳距。起跳力度限制在 0.05 到 1,乘以 240 世界像素得到跳距。预测线和实际落点采用相同的距离与斜率,所以蓄力时看到的落点能和随后起跳的计算对上。飞行按游戏时钟推进 0.5 秒,画面上的抛物线抬升单独计算。
跳台类型按权重抽取,再按各自区间取半宽和间距。大小和间距是两个独立参数,同样的距离遇到窄台,仍然需要更准确的力度。普通台约有 18% 的概率生成高侧面变体,侧面高度只影响绘制,落点仍按台心和半宽判断。各类型参数见第 2 节。
判定使用到台心的二维距离。 记落点到目标台心的距离为 r,目标台顶面半宽为 hw。当 r ≤ max(6, 0.24 × hw) 时判为中心命中,其余落在 1.05 × hw 范围内的判为普通落地,再远则落空。顶面虽然画成圆角菱形,代码使用的是距离阈值。略宽于半宽的外圈为擦边落点留出容差。
落在台边后,如果下一座台换到另一侧,上一跳的站位偏差就会转成新方向上的横向偏移,预测线可能因此偏离目标。
为此,onJumpEnd 先用真实落点判分,updateSettle 再将角色缓动回台心。这样保留了本次落点的判定结果,同时让下一跳从生成目标台时采用的台心出发。
// onJumpEnd:用真实落点判定并记录落地动画起点
const land = landingPoint(this.jump);
const target = this.jump.target;
const result = judgeLanding(land.x, land.y, target);
// ……落空分支直接返回
this.settleFrom = { x: land.x, y: land.y };
// updateSettle:从真实落点移向台心
this.player.x = this.settleFrom.x
+ (block.x - this.settleFrom.x) * eased;
this.player.y = this.settleFrom.y
+ (block.y - this.settleFrom.y) * eased;
2. 跳台奖励、计分与视听反馈
| 跳台 | 生成权重 | 顶面半宽 hw | 台心 x 轴间距 | 停留奖励 |
|---|---|---|---|---|
| 普通台 | 68 | 26–36 | 90–160 | 无 |
| 井盖 | 8 | 20–25 | 100–160 | +5 |
| 魔方 | 8 | 23–28 | 90–150 | +10 |
| 便利店 | 8 | 34–42 | 100–170 | +15 |
| 音乐盒 | 8 | 25–31 | 90–160 | +30 |
表中尺寸为世界像素,权重适用于随机生成的后续跳台。起始台固定为半宽 34 的普通台。特殊台用不同的顶面图案区分。井盖和便利店虽然同为特殊台,半宽区间不同,落地判定的容差也不同。
普通落地加 1 分并清零连击。每次中心命中先把连击数加 1,再按 min(2 × 连击数, 32) 算这一跳的分数,依次为 2、4、6 分,单跳连击分封顶 32。HUD 从两连击开始显示连击数。本局达到 500 分时,预测参考线在 0.8 秒内淡出。这个阈值只改变提示,力度映射和落点判定保持同一套规则。
特殊台的奖励独立于落点分。成功落地后开始按游戏时间计时,在同一座特殊台停满 2 秒就加一次分,同时显示加分和台面脉冲。起跳前如果还没等满两秒,就会错过奖励。额外奖励不会增加或打断连击,因此玩家可以在保持连击的同时选择是否停留。例如第三次中心命中落在音乐盒上,本跳加 6 分,停满两秒再加 30 分。下一跳若仍命中中心,则按第四次连击加 8 分。
图 6 1.0.6 落点与计分分支。连击由落点决定,额外奖励由停留时间决定。
图 7 v1.0.3 录像中的四连击提示。
图 8 v1.0.3 演示视频中的结算页。左侧显示本局得分和个人最佳,右侧榜单使用演示数据。
历史最佳和本月最佳分别保存,只有刷新本月纪录才加入同步队列。失败记录可在回到前台后重试,跨月后不补报旧月成绩。月榜异步加载,离开结算页时取消读榜请求。网络不可用时,如果已有缓存,界面会注明原月份和「上次榜单」。
全屏 Ink 页面管理开局、游戏、暂停和结算。Canvas 绘制跳台、预测线和分数特效,角色用单独的 Ink 图片组件从图集中裁切。这让角色可以独立移动,背景没有变化时可以复用。早期 HUD 文字改用绝对定位后被零高度的父容器裁切,修复时将文字挂到有明确尺寸的舞台上。Canvas 也只画到当前目标台,避免后续跳台提前覆盖分数区。
音频分成三路,分别对应持续的背景、连续变化的力度和离散的游戏事件。
- 背景音乐用独立的 MP3 播放器循环。正式开局和恢复游戏时请求播放,代码将目标音量设为 0.5。这个值是播放器参数,实际响度随设备和运行时表现而定。
- 蓄力音用同一段 WAV 循环。力度达到启动条件后,目标音量按
0.06 + 0.30 × 力度计算。包络每 40 毫秒向目标靠近一半,到达阈值后停止更新。慢抬时音量回落,蓄力结束时淡出。这样可以连续反馈力度,不用每帧重播素材。 - 短音效包括起跳、普通落地、中心命中、奖励和掉落。两组播放器轮换复用,落地按判定选择普通或完美音效。短音效播放失败后冷却 5 秒,游戏照常计分。
暂停、进入后台或结算时,停止蓄力音与短音效并暂停 BGM,卸载时释放播放器。游戏暂停时,跳跃与停留奖励的游戏时钟也一起冻结,恢复后沿用本局的平视基准。
图 9 1.0.6 音频通道。BGM、蓄力循环和短音效各自响应游戏事件。
素材制作。 角色使用项目保留的六张姿态原图,其中两张扑翼帧由项目作者提供。构建脚本把六张原图打成一张图集,运行时按姿态切换裁切区域。五种短音效由项目脚本离线合成,输出为单声道 WAV;眼镜端直接播放这些文件。BGM 使用从原曲截取的约 18 秒循环片段,在循环接点做了交叉混合。蓄力使用独立的循环 WAV。
3. 验证与后续迭代
早期在游戏帧读取角速度时,短峰值可能在两帧之间被低读数覆盖。模拟测试按「越过阈值、读数回落、推进游戏帧」的顺序输入样本,检查起跳事件能否保留并只处理一次。浏览器预览用于检查页面和布局,本次用 1.0.6 源码重新运行了头控、游戏逻辑、页面计分、游戏时钟、预测线阈值、音频生命周期和短音效复用相关的 7 组测试,全部通过。这些测试验证的是输入处理和状态逻辑;文中的真机操作画面来自前述 v1.0.3 演示。
后续计划加入近距离首跳练习。失败时分别提示未触发起跳、跳短或跳过目标,让玩家知道该调整回抬速度还是蓄力幅度,并能立即重试。