Skip to content

告别延迟拖尾:用线性预测实现 Display 实体的低延迟跟随

伊桑桑桑桑桑

伊桑桑桑桑桑

该方法存在缺陷

该方法并不对任意模型生效。详情见方法局限性和编者注。

搞过原版枪械或者自定义 HUD 的人都知道,想让一个 item_display 死死跟在玩家视角前面有多难。

最粗暴的做法是每 tick 执行 tp @e ^ ^ ^。但这有个两难的问题:如果不给 teleport_duration,实体移动没有插值,只有 20fps 的刷新率,看着非常卡顿;如果给了 teleport_duration: 1 强行做平滑,因为客户端播动画需要耗时(50ms),只要玩家一跑动或者转头,模型就会慢半拍,产生极其难受的拖尾。

这篇文章分享一个思路,通过利用客户端渲染特性和简单的线性运动预测,把这个延迟基本“吃掉”。

1. 视角旋转:别用服务端算

很多人试图用高频 tp 来强行同步玩家的视线角度,但这部分其实完全可以白嫖客户端本身的渲染机制。

给 Display 实体加上 billboard: "center",它就会永远在客户端层面自动面朝玩家屏幕。如果想让模型偏离屏幕中心(比如把枪放在右下角),去调 transformation.translation 矩阵,绝对不要用 ^ ^ ^ 算出来的绝对坐标去控制偏移。

把旋转交给客户端后,无论玩家鼠标甩得多快,模型在屏幕上的相对位置都是绝对静止的,零延迟。

2. 移动跟随:预测下一帧

视角的问题解决了,剩下就是坐标的移动。为什么加了插值就会拖尾?因为当服务端命令实体往当前坐标走的时候,客户端需要花时间播动画。等它走到了,玩家早就走到下一个位置了。

所以核心思路是:预测。假设玩家在一瞬间是在做匀速直线运动,我们不让实体飞向玩家当前的位置,而是直接把它 tp 到玩家下一 tick 要去的位置。这样客户端慢吞吞播插值动画的时候,刚好能跟玩家实际的移动轨迹重合。

公式很简单:设玩家上一 tick 的位置是 A(也就是实体当前显示的起点),当前 tick 的位置是 B。 这一帧的位移(速度) V=BA。 我们要预测下一帧的目标位置 C=B+V=2BA

运行流程也就变成了:每 tick 算出 2BA,把实体 tp 过去,然后把这一帧的玩家位置存下来,留给下一帧当 A 用。

3. 宏指令里的坐标减法(空间翻折法)

2BA 这个式子用积分版算当然没有问题,但是有没有更精确的办法呢?

我们可以利用宏来实现。在原版命令里,坐标加法用宏很好写,positioned ~$(B_x) ~$(B_y) ~$(B_z) 就行。但减法没法直接写,因为如果 A 传进来的坐标是个负数(比如 -50),宏替换进 ~-$(A_x) 就会变成非法的 ~--50,直接报错。

这里分享一个利用局部坐标配合视角旋转来做纯几何减法的技巧:不用管正负号,直接把局部坐标轴“翻过去”对准全局坐标的负方向。

mcfunction
# 假设 target_xyz 代表当前位置 B
# prev_xyz 代表上一帧位置 A

positioned 0. 0. 0. rotated 180 0 run function latch:follower/__tick__/run_at_owner_eyes_tp with storage latch:io temp.vars:
    $execute \
        positioned ~$(target_x) ~$(target_y) ~$(target_z) \
        positioned ~$(target_x) ~$(target_y) ~$(target_z) \
        positioned ^$(prev_x) ^ ^$(prev_z) \
        rotated 0 90 positioned ^ ^ ^$(prev_y) \
        run function latch:follower/tp_here

原理解释:

  1. 前两次 positioned ~$(target_x)... 是普通的坐标叠加,算出了 2B
  2. 此时执行环境由于开头的设定,是 rotated 180 0(水平向后转)的视角。在这个视角下,局部坐标的左方(^X)和前方(^Z)刚好对应全局坐标的 -X 和 -Z。所以执行 positioned ^$(prev_x) ^ ^$(prev_z) 时,不管传进来的参数是正是负,都等价于在全局坐标里减去了 AxAz
  3. 同理,接着用 rotated 0 90 垂直低头看地,局部坐标的前方(^Z)就对应了全局的 -Y。再执行 positioned ^ ^ ^$(prev_y) 就相当于减去了 Ay

利用这种空间翻折,短短几行代码就搞定了带负数的精确的向量减法。

4. 方案的局限

当然,预测算法不是万能的,这套方案在实际游玩中有两个客观存在的小毛病:

  • 急停过冲:因为是根据历史速度盲猜的,当玩家疾跑中突然停下时,实体会因为“惯性”往前稍微冲一下,下一 tick 才会弹回正确位置。
  • 吃 TPS / 帧数:这种做法高度依赖稳定的 tick 轴。如果服务器卡顿或者客户端严重掉帧,预测轨迹就会出现视觉偏差(不过说实话,服务器卡的时候,传统的直接 tp 方案一样没法看)。

抛开这两点,这套方法目前在单人地图里效果是还不错的,推荐在开发第一人称手臂模型之类的东西的时候尝试一下。

编者注

该方法仅对原版默认物品模型有最佳效果,使用自定义物品模型需要保证billboard旋转枢轴点在玩家眼部位置。
至于如何保证这一点,此原理暂未探明,留给读者探索;

Powered by Vitepress and Github Pages