Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

高级物理插值

虽然前面的说明在很多游戏中都能给出令人满意的结果,但在某些情况下,你可能需要更进一步,以获得最佳效果和最流畅的体验。

自动物理插值的预期

即使启用了物理插值,在某些局部情况下,你可能需要禁用 Node(或 SceneTree 分支)的自动插值,转而手动执行插值以获得更精细的控制。

这可以通过所有节点中都存在的 Node.physics_interpolation_mode 属性来实现。例如,如果你为一个节点关闭了插值,其子节点也会递归地受到影响(因为它们默认继承父节点的设置)。这意味着你可以轻松地为整个子场景禁用插值。

../../../_images/physics_interpolation_mode.webp

值得注意的是,无论是在 2D 还是 3D 中,物理插值都是在每个实例的本地变换(local transform)上进行的。在渲染阶段,插值后的本地变换会被传递给其子节点。

这意味着,如果父节点的 physics_interpolation_mode (物理插值模式)被设为 On (开启),但子节点被设为 Off (关闭),那么当父节点移动时,子节点依然会被插值处理。唯一不进行插值的,只有子节点自身的本地变换。 因此,在规划节点的开/关行为时,需要多加考量和布局。

最常见的需要你自己进行插值的情况是相机(Cameras)。

相机

在许多情况下,Camera3D 可以像其他节点一样使用自动插值。然而,为了获得最佳效果,特别是在物理帧率较低时,建议你采用手动的方式进行相机插值。

这是因为观看者对相机的移动非常敏感。例如,一个每 1/10 秒(10tps 的周期速率)轻微重新对齐的 Camera3D 常常会被注意到。你可以通过在 _process 中每帧移动摄像机,并手动跟随一个插值的目标来获得更平滑的结果。

手动相机插值

确保相机使用全局坐标空间

执行手动相机插值的第一步是确保 Camera3D 的变换是在全局空间中指定的,而不是继承移动父节点的变换。这是因为 Camera3D 的父节点移动与摄像机节点本身的移动之间可能会产生反馈,从而干扰插值效果。

有两种方法可以做到这一点:

  1. 将 Camera3D 移动到一个独立的分支上,而不是作为移动对象的子节点。

../../../_images/fti_camera_worldspace.webp
  1. 调用 Node3D.top_level 并将其设为 true,这将使 Camera 忽略其父节点的变换。

典型案例

一个典型的自定义方法是,在 _process() 函数中每一帧都使用 Camera3D 的 look_at 函数来注视目标节点(例如玩家)。

但是这里有一个问题。如果我们在 Camera3D 的“目标”节点上使用传统的 get_global_transform(),这个变换只会让 Camera3D 在当前物理帧聚焦于目标。这不是我们想要的,因为此时当目标移动时,相机会在每一物理帧跳动。即使相机可能每帧更新,但如果目标只在每个物理帧发生变化,这并不能帮助实现平滑的运动。

get_global_transform_interpolated()

我们真正想要让相机聚焦的,并不是目标在物理帧上的位置,而是插值后的位置,即目标将被渲染的位置。

我们可以使用 Node3D.get_global_transform_interpolated 函数来实现这一点。它的作用与获取 Node3D.global_transform 完全相同,但它会给你插值后的变换(在 _process() 调用期间)。

重要

get_global_transform_interpolated() 应该只在一两个特殊情况下使用,例如相机。在你的代码中不应该到处使用这个方法(既是为了性能考虑,也为了保证正确的游戏玩法)。

备注

除了像相机这样的特殊情况外,在大多数情况下,你的游戏逻辑应该放在 _physics_process() 中。在游戏逻辑中,你应该调用 get_global_transform()get_transform(),它们会返回当前的物理变换(分别是在全局或局部空间中),这通常是你在游戏逻辑代码中所需要的。

手动相机示例脚本

以下是一个跟随插值目标的简单固定相机示例:

extends Camera3D

# 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 Camera3D,
    # 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))

鼠标运动

鼠标视角控制是一种非常常见的相机控制方式。但这里存在一个问题。与可以在物理帧中周期性采样的键盘输入不同,鼠标移动事件可能会持续不断地产生。相机需要在下一帧立即响应并跟随这些鼠标移动,而不是等待到下一个物理帧。

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

有时,特别是在处理相机时,你可能希望同时使用插值和非插值:

相机类型有许多排列组合和变体,但很明显在许多情况下,禁用自动物理插值并自行处理可以获得更好的效果。

在其他节点上禁用物理插值

尽管摄像机是最常见的例子,但在某些情况下,你可能希望其他节点控制它们自己的插值,或者不使用插值。例如,考虑一个俯视角游戏中的玩家,其旋转由鼠标控制。禁用物理旋转可以让玩家的旋转实时匹配鼠标的移动。

MultiMesh

尽管大多数视觉节点遵循单个节点对应单个视觉实例的模式,但 MultiMesh 可以从同一个节点控制多个实例。因此,它们具有一些额外功能,可以基于每个实例来控制插值功能。如果你正在使用插值的 MultiMesh,你应该探索这些功能。

详情见 MultiMesh 文档。