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.
Checking the stable version of the documentation...
高级物理插值
虽然前面的说明在很多游戏中都能给出令人满意的结果,但在某些情况下,你可能需要更进一步,以获得最佳效果和最流畅的体验。
自动物理插值的预期
即使启用了物理插值,在某些局部情况下,你可能需要禁用 Node(或 SceneTree 分支)的自动插值,转而手动执行插值以获得更精细的控制。
这可以通过所有节点中都存在的 Node.physics_interpolation_mode 属性来实现。例如,如果你为一个节点关闭了插值,其子节点也会递归地受到影响(因为它们默认继承父节点的设置)。这意味着你可以轻松地为整个子场景禁用插值。
值得注意的是,无论是在 2D 还是 3D 中,物理插值都是在每个实例的本地变换(local transform)上进行的。在渲染阶段,插值后的本地变换会被传递给其子节点。
这意味着,如果父节点的 physics_interpolation_mode (物理插值模式)被设为 On (开启),但子节点被设为 Off (关闭),那么当父节点移动时,子节点依然会被插值处理。唯一不进行插值的,只有子节点自身的本地变换。 因此,在规划节点的开/关行为时,需要多加考量和布局。
最常见的需要你自己进行插值的情况是相机(Cameras)。
相机
在许多情况下,Camera3D 可以像其他节点一样使用自动插值。然而,为了获得最佳效果,特别是在物理帧率较低时,建议你采用手动的方式进行相机插值。
这是因为观看者对相机的移动非常敏感。例如,一个每 1/10 秒(10tps 的周期速率)轻微重新对齐的 Camera3D 常常会被注意到。你可以通过在 _process 中每帧移动摄像机,并手动跟随一个插值的目标来获得更平滑的结果。
手动相机插值
确保相机使用全局坐标空间
执行手动相机插值的第一步是确保 Camera3D 的变换是在全局空间中指定的,而不是继承移动父节点的变换。这是因为 Camera3D 的父节点移动与摄像机节点本身的移动之间可能会产生反馈,从而干扰插值效果。
有两种方法可以做到这一点:
将 Camera3D 移动到一个独立的分支上,而不是作为移动对象的子节点。
调用 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 中应用。
有时,特别是在处理相机时,你可能希望同时使用插值和非插值:
第一人称摄像机可能会将摄像机定位在玩家位置(可能使用 Node3D.get_global_transform_interpolated),但会根据鼠标视角控制摄像机旋转而不使用插值。
第三人称摄像机可以类似地使用 Node3D.get_global_transform_interpolated 确定摄像机的注视点(目标位置),但使用鼠标视角来定位摄像机位置时不进行插值。
相机类型有许多排列组合和变体,但很明显在许多情况下,禁用自动物理插值并自行处理可以获得更好的效果。
在其他节点上禁用物理插值
尽管摄像机是最常见的例子,但在某些情况下,你可能希望其他节点控制它们自己的插值,或者不使用插值。例如,考虑一个俯视角游戏中的玩家,其旋转由鼠标控制。禁用物理旋转可以让玩家的旋转实时匹配鼠标的移动。
MultiMesh
尽管大多数视觉节点遵循单个节点对应单个视觉实例的模式,但 MultiMesh 可以从同一个节点控制多个实例。因此,它们具有一些额外功能,可以基于每个实例来控制插值功能。如果你正在使用插值的 MultiMesh,你应该探索这些功能。
详情见 MultiMesh 文档。