终末地官表计时机制与潜在偏差研究
写在前面§
最近看到社区里关于《明日方舟:终末地》官表和“村规”的讨论很多,一时兴起,便尝试从代码层面逆向分析了关卡计时器和部分机制的底层原理。
必须说明的是,我个人并不是竞速玩家,对竞速社区的了解不够深入;加上游戏代码量极其庞大,个人精力有限,几天分析下来也只是窥见冰山一角。内容若有不妥或错误之处,还请大家包涵并多多指正。
受朋友所托写下此文,主要是想从底层逻辑聊聊官表计时可能存在的局限性,借此抛砖引玉,希望能和大家一起探讨,推动社区形成一套更完善、有共识的竞速规则。
一、关卡计时器的大致流程§
终末地副本关卡最终结算时间以服务端下发的 passtime 为准,服务端是整个计时的权威状态,而服务端的计时则依赖于客户端的消息上报。
关卡计时流程由服务端下发挑战启动的消息开始,客户端收到该消息后以 challengeStartTs 为开始时间启动计时。ESC暂停、客户端播放CG等等行为均会导致游戏时间暂停并触发客户端事件上报,服务端会根据上传的事件累计客户端的暂停时间。战斗结束之后,服务端会下发挑战完成的消息,在该消息内会携带 passtime 并在最后挑战完成的页面展示总用时。
二、服务端是怎么计时的?§
需要先说明:我们无法访问游戏的服务端代码,因此无法直接验证服务端的具体计时实现。下文涉及服务端逻辑的部分,均基于客户端逆向结果及双端逻辑一致性的合理假设,属于推断而非定论。
客户端的计时器分成三种:累加计时,限时倒计时和截止时间。以下先据此还原客户端逻辑,再推测服务端可能采用的计时方式。
累加计时的计时逻辑为:
消耗时间 = 服务端下发的关卡结束时间 − 客户端本地时间 + 累计暂停时长
限时倒计时的计时逻辑为:
剩余时间 = 客户端本地时间 − 服务端下发的关卡开始时间 − 累计暂停时长
截止时间的计时逻辑为 :
完成时间 = 服务端下发的关卡结束时间 − 累计暂停时长
需要注意的是,三种计时器的舍入方式也存在区别:累计加时采用向下取整,限时倒计时采用向上取整,截止时间采用四舍五入。
若服务端采用与客户端相近的计时原则,其耗时计算可能近似为:
服务端关卡关闭时间 − 服务端关卡开启时间 − 客户端上报的累计暂停时长
三、客户端的时间修正§
在理想条件下,客户端与服务端时间一致,且网络不存在延迟、丢包或路径不对称,服务端计算关卡耗时会较为直接。

若服务端的计时过程依赖客户端上报事件,那么客户端与服务端的时间偏差就可能影响最终结果,如下图中出现的场景,客户端瞎传暂停时间戳:

显然,游戏需要避免这种明显的时间偏差,因此客户端设置了一套时间校准机制。
第一步:登录阶段的时间校准
收到登录响应后,客户端根据其中的服务端时间戳(毫秒)和时区信息,估算服务端与本地时钟之间的偏差。大致逻辑如下:
candidateBias = serverTimeMS - GetCurrentTimestampByMilliseconds()
注:GetCurrentTimestampByMilliseconds() 获取的是客户端本机当前时间戳(毫秒)。
当 candidateBias 的绝对值大于 180000 ms(即 3 分钟)时,客户端才进行登录阶段的时间偏差校准。
第二步:运行时的时间校准
客户端会周期性发送 CS_PING,其中携带发送时的设备 UTC 时间戳(毫秒)。服务端在 SC_PING 响应中回显该客户端时间戳,并附带服务端时间戳。大致逻辑如下:
receiveTs = DeviceUTCTimestampByMilliseconds()
rtt = max(receiveTs - sessionThreadSleepMs - echoedClientTs, 1)
candidateBias = serverTs + floor(rtt / 2) - receiveTs
客户端收到 SC_PING 后,先记录当前设备的 UTC 时间戳 receiveTs。随后用当前接收时间减去发送 PING 时的客户端时间戳 echoedClientTs,并扣除代码中记录的 sessionThreadSleepMs,估算网络往返时间 RTT。
在上、下行延迟近似对称的情况下,取 RTT 的一半作为单程延迟的近似值。将这段延迟加到服务端时间戳 serverTs 上,再与客户端当前时间进行比较,即可得到服务端时间相对于客户端时间的偏差 candidateBias。
若当前记录的时间偏差与本次估计偏差之间的差值超过 1000 ms,则更新客户端记录的服务端时间偏差。
举个例子:
服务端时间 1000 ms 发 SC_PING,客户端本地时间 950 ms 收到
单程延迟 x=50 ms →
bias = 1000 + 50 − 950 = 100 ms
即服务端快 100 ms
客户端此刻校准时间 = 950 + 100 = 1050 ms
而服务端此刻确实是 1000+50=1050 ms,完全吻合。
当上、下行延迟不对称时,RTT / 2 不再等于实际下行延迟,校准会引入误差。按公式,估计误差为 (上行延迟 − 下行延迟) / 2。因此,它不一定总会拉大原有偏差,但会使校准结果向偏快或偏慢的一侧偏移。
再举个例子:
场景 | 上行 x₁ | 下行 x₂ | 误差 |
|---|---|---|---|
上行延迟高、下行延迟低 | 490 ms | 10 ms | +240 ms(校准时间偏快) |
上行延迟低、下行延迟高 | 10 ms | 490 ms | −240 ms(校准时间偏慢) |
四、被截断的时间§
这一点无需展开。在第二小节中已经有描述,这里摘抄如下:
客户端的计时器分成三种:累加计时,限时倒计时和截止时间。
三种计时器的舍入方式也存在区别:累计加时采用向下取整,限时倒计时采用向上取整,截止时间采用四舍五入。
五、跳表问题§
前面铺垫了那么多,终于可以开始正餐了。首先是这个跳表问题,就是游戏结束时,玩家HUD界面上展示的时间和最终结算时展示的时间不一致:“有时候会多一秒,有时候会少一秒。”
我们来回忆一下前面写过的内容。 首先,关卡最终结算的时间是服务端统计的时间,接着服务端统计时间需要依赖客户端上报的时间,然后服务端和客户端的时间可以有 1s 的偏差,在网络条件差的情况下能更多…..,最后配上一点调料——被截断的时间。
若网络因素使客户端统计时间小于服务端统计时间:客户端统计为 8.5 s,HUD 显示为 8 s;服务端统计为 9.0 s,最终结算显示为 9 s。

若网络因素使客户端统计时间大于服务端统计时间:客户端统计为 9.2 s,HUD 显示为 9 s;服务端统计为 8.7 s,最终结算显示为 8 s。

前文提到,若服务端按客户端上报的暂停事件参与计时,那么暂停事件到达服务端的时机也可能成为变量。
至于图示场景应如何结算,暂且留作讨论
提示:以下讨论涉及潜在游戏漏洞。请勿在正式竞速或其他可能影响公平性的场景中尝试。

对官表的讨论暂告一段落。接下来,将分析竞速社区中部分“村规”背后的机制。
六 、以罗丹为例,分析 ESC 开与 P 开的区别§
先介绍一下 ESC 开和 P 开的区别,ESC 开指的是使用 ESC 键退出副本后重新进入副本进行挑战,而 P 开指的是使用 P 键重置副本进行挑战。由此可知 ESC 开与 P开,游戏内的加载流程可能是不一样的,当然也确实不一样。
首先是 ESC 开,在这种情况下,游戏会进入下图所示的加载页面,进行场景加载等必要的工作,加载界面结束后,战斗即可开始。

P 开的流程则不同,由于玩家已处于关卡场景中,客户端需要重置怪物、玩家站位等关卡状态,客户端会使用黑屏遮罩来进行过渡。
而问题就发生在这个黑屏里的游戏时序。关卡重新加载完毕后,客户端会通知服务端,但是客户端开始计时需要服务端下发的挑战开始的消息。这段通信存在客观延迟,但是客户端会保留约 0.5 s 的淡出黑屏;若服务端的关卡开始指令在黑屏尚未结束时到达,客户端虽已开始刷怪,黑屏却仍可能持续。

举个例子:
假设 P 开后存在 0.5 s 黑屏,且服务端的关卡开始指令在 80 ms 后到达客户端,那么黑屏结束前仍会剩余约 0.42 s 的等待时间。在 ESC 开不存在等效等待的前提下,这部分时间可能构成 P 开相较 ESC 开的额外延迟。
此外,服务端与客户端之间的通信延迟会随网络状况变化,因此该处额外等待时间也存在相应波动。
写在最后§
从周五晚上开始到现在,断断续续分析了几天代码,研究周期比预想更长。起初我还计划分析白垩相关机制、聂菲斯跳过场动画时的计时问题,以及连携冻屏等案例;但目前证据仍不足以支撑严谨结论。为保证文章质量,这些内容暂不展开,待研究充分后再另文补充。
最后想说的是,发现计时机制的局限并不可怕,关键在于如何建立公平、可验证的规则。希望本文能引发更多讨论,让社区在充分理解游戏底层限制的基础上,以友好沟通和共同验证的方式,形成一套更完善、更具共识的竞速规则。