每块屏幕上都是同一艘飞船

只有当飞过同一段洞穴在老旧的小手机、144Hz 平板和桌面浏览器上难度相同时,每日排行榜才是公平的。以下是为此所做的工作,以及哪些方面我无法做到完全相同。

開發日誌 · 2026 年 9 月 20 日 · 所有文章

关于每日洞穴的文章讲的是让每个玩家的世界保持一致。这篇讲的是公平性的另一半:让飞船表现得完全一致。设备能以两种独立的方式改变游戏的玩法,它们需要两种独立的修复。一种是屏幕刷新的频率,另一种是屏幕的大小。

问题一:帧率

飞船有一个位置和一个速度。每一帧,重力或推力会改变速度,速度又会改变位置。写这段代码的显而易见的方式是:

vy += accel * dt;     // update velocity
py += vy * dt;        // then move by the NEW velocity

这看起来无害,实际上却存在一个微妙的错误。第二行假装飞船在整个这一帧里都已经以帧末的速度在移动。如果飞船正在加速,这每一帧都会多算出 accel × dt² 的一半。麻烦在于,帧长 dt 在每台设备上都不一样,所以多算出的量也不一样,于是飞船在每种刷新率下飞出的轨迹都略有不同。

我用保持推力半秒钟的方式测量过。精确答案是 225 像素。旧的积分方法在 144Hz 下给出 228,60Hz 下给出 232,30Hz 下给出 240。三台设备,相同的输入,却是三个不同的距离。

修复方法是梯形法则,它使用该帧前后速度的平均值:

const vyPrev = vy;
vy += accel * dt;
py += (vyPrev + vy) * 0.5 * dt;   // average speed over the frame

对于恒定加速度而言,无论帧长多少,这都是精确的。重放同一套输入序列,现在在 12Hz 到 144Hz 之间的差异是 0.000 像素。之前是 4.1 像素。有一个测试同时验证两点:新的积分方法在所有刷新率下都一致,以及旧的积分方法确实存在偏差,这样如果有人把它改回去,测试就会失败,而不是让排行榜悄悄跑偏。

两个诚实的但书

它并没有缩小不同刷新率之间的分数差距。我原本也预料到了这一点。修复之后,模拟飞行员在 144Hz 下的得分仍然比在 60Hz 下高出约三分之一。那个差距不是物理问题,而是输入分辨率问题:拇指只能在帧边界上从按住切换到松开,所以更快的屏幕确实能提供更精细的控制。任何积分方法都无法解决这一点,而每帧都反转输入的机器人会比人类更放大这种差异。物理这一半现在已经精确了,剩下的部分是硬件本身的特性。

在 60Hz 下这是一个非常轻微的削弱。半秒钟的爬升现在只能覆盖 225 像素,而不是 232 像素。我有意没有补偿这一点。如果真机测试有一天发现飞船感觉迟钝,杠杆就是推力常数,大约多加 3% 就能恢复原来那个半秒钟的动作。这也正好提醒一句:推力此前已经因玩家反馈被改动过两次,那段故事正是我对再次改动它如此谨慎的原因。

问题二:屏幕高度

以每秒像素平方为单位的重力,在高度 375 点的手机和高度 520 点的平板上意味着截然不同的东西。如果常数保持不变,同样的爬升在平板上穿过的屏幕比例会小得多,游戏也会显得更沉重。

因此这些常数是相对于 440 点的参考高度定义的,这是最大 iPhone 横屏时的高度,游戏的手感正是在这个高度上调校和测试的。在任何其他屏幕上,重力、推力和终端速度都会乘以 H / 440。由于通道的半间隙也会随高度缩放,每条轨迹在几何上都与参考轨迹相似:每秒穿过通道的比例相同,从一侧墙到另一侧墙所需的时间相同,体感的反应速度也相同。更大的屏幕会得到更陡的像素加速度,更小的屏幕则会得到更平缓的加速度,这是刻意设计的结果。

这也带来了一条适用于所有未来代码的规则:任何涉及垂直运动判断的逻辑都必须基于比例。是终端速度的一个比例,或者高度的一个比例。绝不能用一个原始的每秒像素常数去和飞船的速度比较,因为那个数字只在一台设备上是正确的。如果实在无法避免使用字面量,就要乘以同样的缩放比例。

网页版遵循同样的规则。浏览器构建将画布限制在 956x440,与那台参考手机的尺寸完全相同,因此浏览器中的游戏在像素和物理上都是它的精确复刻。早期的网页构建有自己独立的手感倍率,那个倍率的存在只是为了对抗另一种不同限制所导致的软绵绵手感,一旦限制对齐后就被删除了。

问题三:屏幕宽度

高度是垂直轴。宽度是水平轴,而且更加微妙。障碍物在世界坐标中以固定的距离分布。洞穴以一个包含屏幕宽度因子的速度滚动。所以在更宽的屏幕上,同样的障碍物在屏幕意义上掠过得更快,你对每个障碍物做出反应的时间会随宽度成比例地减少。

这是一个真实存在的漏洞。一台宽约 1280 点的大型安卓平板,面对共享洞穴的速度大约是小手机的 1.75 倍。因此宽度在每个平台上都被限制在 956 点,也就是横屏时最宽的 iPhone 的宽度。比这更宽的设备左右两侧会出现黑边。这在 iPhone 上没有任何作用,只会限制大型安卓设备和桌面浏览器。

这个上限还留下了一个我尚未修复的残余问题。玩家的碰撞半径随宽度缩放,而通道随高度缩放,因此在更接近正方形的平板上,飞船相对于间隙会比在手机上略小一些,宽容度大约高出 20%。修复这个问题意味着要改变碰撞判定框,这是一个需要单独进行游戏测试的手感变化,而且它偏向宽容的方向,所以可以先放一放。

用测试来验证公平性,而不是仅仅断言它

这一切在游玩时都是不可见的。你感觉不到几个像素的偏差,也没有人会同时在六台设备上玩同一个洞穴。所以每一条声明都配有一个重放它的测试:物理测试在 12Hz 到 144Hz 的帧率下对固定输入进行积分,洞穴测试则在六种设备尺寸上重放真实的生成器,并逐字节比对结果。根据我的经验,一个未经测试的公平性属性,早已在悄悄地被打破。

简而言之,TUNL 在三个独立的方面(帧率、高度和宽度)上做到了跨屏幕公平,每一方面都有不同的成因、不同的修复方式和不同的残余问题。你可以在自己拥有的任何设备上,在今天的洞穴里亲自看到结果。

遊玩今天的洞穴