高级物理插值

尽管之前的做法可以在大多数游戏中达到令人满意的结果,但在某些情况下,你会想要更进一步,获得最佳结果和最丝滑的体验。

自动物理插值的例外情况

即便启用了物理插值,局部也可能存在一些情况,需要你为某个 Node(或者 SceneTree 的分支)禁用自动插值,去精确控制手动插值。

这可以通过所有 Node 都有的 Node.physics_interpolation_mode 属性实现。假设你关掉了某个 Node 的插值,那么它的所有子节点都会受到影响(因为默认是继承父级设置的)。也就是说,要禁用整个子场景的插值非常简单。

最常见的情况就是你可能会想要在 Camera 上执行自己的插值。

相机

在很多情况下,Camera 是可以和其他节点一样自动插值的。然而为了达到最佳的效果,尤其是在物理周期率较低的情况下,建议你手动对 Camera 进行插值。

这是因为观察者对 Camera 的移动是非常敏感的。例如,Camera 每十分之一秒(10 TPS 周期率)做一次略微的重对齐就很容易发现了。通过将 Camera 在 _process 里做每帧的移动,手动跟随插值目标,就可以获得更顺滑的结果。

手动 Camera 插值

请确保 Camera 使用的是全局坐标空间

执行手动 Camera 插值的第一部就是确保 Camera 的变换是用全局空间表达的,而不是继承自可移动的父节点的变换。这是因为 Camera 父节点的运动可能影响到这个 Camera Node 本身的移动,打乱插值。

有两种实现方法:

  1. 调整 Camera 的位置,让它位于自己独立的分支上,不再作为会移动对象的子节点。

../../../_images/fti_camera_worldspace.png
  1. 调用 Spatial.set_as_toplevel 并将其设置为 true,就会让 Camera 忽略父级的变换。

典型示例

自定义方法的典型例子就是每一帧在 _process() 里使用 Camera 的 look_at 函数让它看向某个目标节点(例如玩家)。

但问题来了。如果我们在 Camera 的“目标”Node 节点上使用传统的 get_global_transform(),这个变换只会在当前物理周期中让 Camera 在这个目标上聚焦。我们并不希望这样,因为目标是会动的,Camera 在各个物理周期中就会产生跳动。尽管 Camera 可能每帧都在更新,但如果目标只在物理周期内变化,那么相机的运动就不可能变得平滑。

get_global_transform_interpolated()

我们真正想要让 Camera 聚焦的,并不是物理周期中目标的位置,而是插值后的位置,即目标渲染的位置。

我们可以使用 Spatial.get_global_transform_interpolated 函数来达到目的。这个函数和 Spatial.global_transform 是一样的,只不过返回的是(在 _process() 调用时)插值后的变换。

重要

只有在类似 Camera 这种特殊的情况下才应该使用一两次 get_global_transform_interpolated()不应该在代码里到处使用(既影响性能,又影响游戏的正确性)。

备注

除了像 Camera 这种例外情况,在大多数时候,你的游戏逻辑都应该放在 _physics_process() 中。游戏逻辑中,你应该调用 get_global_transform() 或者 get_transform() 获取当前的物理变换(分别对应全局和本地),游戏代码中要用到的一般就是这两个。

示例手动 Camera 脚本

这是一个简单的示例,演示跟随插值对象的固定 Camera:

extends Camera

# Node that the camera will follow
var _target

# We will smoothly lerp to follow the target
# rather than follow exactly
var _target_pos : Vector3 = Vector3()

func _ready() -> void:
        # Find the target node
        _target = get_node("../Player")

        # Turn off automatic physics interpolation for the Camera,
        # we will be doing this manually
        set_physics_interpolation_mode(Node.PHYSICS_INTERPOLATION_MODE_OFF)

func _process(delta: float) -> void:
        # Find the current interpolated transform of the target
        var tr : Transform = _target.get_global_transform_interpolated()

        # Provide some delayed smoothed lerping towards the target position
        _target_pos = lerp(_target_pos, tr.origin, min(delta, 1.0))

        # Fixed camera position, but it will follow the target
        look_at(_target_pos, Vector3(0, 1, 0))

鼠标控制视角

鼠标控制视角是控制 Camera 的常见方法。但问题来了,和能够在物理周期中反复采样的键盘输入不同,鼠标的移动事件是持续发生的。Camera 应该在下一帧就立马做出反应,跟随鼠标移动,而不是等到下一个物理周期再动。

在这种情况下,最好就是禁用 Camera 节点的物理插值(使用 Node.physics_interpolation_mode),直接将鼠标输入应用到 Camera 的旋转中,不要在 _physics_process 里应用。

有些时候,尤其是涉及 Camera 时,你可能会希望同时使用插值和非插值:

  • 第一人称视角可能将相机放置在玩家的位置(使用的可能是 Spatial.get_global_transform_interpolated),但 Camera 的旋转通过鼠标来控制,不进行插值。

  • 第三人称视角可能也用类似的方法使用 Spatial.get_global_transform_interpolated 来设置相机的朝向(目标位置),但放置这种使用鼠标控制视角的相机时,是不能使用插值的。

Camera 的类型有多种不同的排列和变种,但应该清楚的是,在大多数情况下,禁用物理插值自己进行处理都能够得到更好的结果。

在其他节点上禁用插值

虽然 Camera 是最常见的例子,但其他节点需要控制自己差不插值的例子也还有不少。例如考虑一下俯视角游戏里面的玩家,其旋转就是通过鼠标来控制的。禁用物理旋转可以让玩家的旋转实时地匹配鼠标的位置。

MultiMesh

尽管大多数可视化 Node 都遵循一个视觉实例一个 Node 的范式,MultiMesh 可以使用单一 Node 控制多个实例。因此,它有一些额外的函数将插值的控制功能精确到某个实例。使用 MultiMesh 进行插值时应当研究一下这些函数。

  • MultiMesh.reset_instance_physics_interpolation

  • MultiMesh.set_as_bulk_array_interpolated

MultiMesh 的文档中有详细说明。