使用 AnimationTree

前言

通过 AnimationPlayer,Godot 拥有你在所有游戏引擎中能找到的最灵活的动画系统之一。几乎可以在任何节点或资源中对任何属性进行动画处理,以及专门的变换、贝塞尔、函数调用、音频和子动画轨道,这样的能力相当独特。

然而, 通过 AnimationPlayer 混合这些动画的支持相对有限, 只能设置固定的交叉渐变过渡时间.

AnimationTree 是Godot 3.1中引入的一个新节点, 用于处理更高级的变换, 它取代了旧的 AnimationTreePlayer , 同时增加了大量的功能和灵活性.

创建动画树

首先, 必须明确 AnimationTree 节点不包含它自己的动画. 相反, 它使用包含在 AnimationPlayer 节点中的动画. 通过这种形式, 你可以像往常一样编辑动画(或从3D场景导入动画), 然后使用这个额外节点来控制播放.

在3D场景中经常使用 AnimationTree . 当从3D交换格式导入场景时, 它们通常自带动画(要么是多个, 要么是在导入时从一个大的动画中拆分出来). 最后, 导入的Godot场景在 AnimationPlayer 节点中包含动画.

很少在Godot中直接使用导入的场景(它们要么实例化, 要么来自继承), 你可以将 AnimationTree 节点放置在包含导入的新场景中. 然后, 将 AnimationTree 节点指向导入场景内创建的 AnimationPlayer 节点.

这是在 第三人称射击游戏演示, 中的设置, 参考下图:

../../_images/animtree1.png

为玩家创建了一个以 KinematicBody 为根节点的新场景,在这个场景中,已实例化原来的 .dae (Collada)文件,并创建 AnimationTree 节点。

创建树

可以在 AnimationTree 中使用三种主要节点类型:

  1. 动画节点,从链接的 AnimationTree 中引用动画。

  2. 动画根节点, 用于混合子节点.

  3. 动画混合节点,在 AnimationNodeBlendTree 中使用,通过多个输入端口进行单图混合。

AnimationTree 中设置根节点, 如下几种类型可供选择:

../../_images/animtree2.png

  • AnimationNodeAnimation:从列表中选择一个动画并播放它. 这是最简单的根节点, 一般不直接用作根节点.

  • AnimationNodeBlendTree:包含许多混合类型的节点,如调配, 混合2, 混合3, 一对一等. 这是最常用的根节点之一.

  • AnimationNodeStateMachine:将多个根节点作为图中的子节点. 每个节点作为一个 状态 使用, 并提供多个函数在状态之间进行切换.

  • AnimationNodeBlendSpace2D:允许在二维混合空间中放置根节点. 在二维中控制混合位置以混合多个动画.

  • AnimationNodeBlendSpace1D:以上的简化版本(一维)。

混合树

AnimationNodeBlendTree 可包含用于混合的根节点和常规节点。节点从菜单添加到图中:

../../_images/animtree3.webp

所有混合树默认都包含一个 Output(输出)节点,为了让动画播放,必须有个东西与其相连。

测试此功能最简单的方法是直接连接一个 Animation(动画)节点:

../../_images/animtree4.png

这会简单地回放动画. 确保 AnimationTree 节点对实际发生的事情是激活的.

以下是可用节点的简短描述:

混合2/混合3

这些节点将通过用户指定输入的两个或三个混合值之间进行混合:

../../_images/animtree5.gif

对于更复杂的混合, 建议使用混合空间.

混合也可以使用过滤器, 也就是说, 你可以单独控制通过混合功能的轨道. 这对于动画的层叠非常有用.

../../_images/animtree6.png

OneShot

此节点将执行子动画, 并在完成后返回. 可以用于定制淡入淡出时间, 以及过滤器.

../../_images/animtree6b.gif

在设置时间和改变动画播放后,播放节点会通过将其 request 值设置为 AnimationNodeOneShot.ONE_SHOT_REQUEST_NONE/ 做到在下一个进程帧自动清除请求。

GDScriptC#

  1. # Play child animation connected to "shot" port.
  2. animation_tree.set("parameters/OneShot/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
  3. # Alternative syntax (same result as above).
  4. animation_tree["parameters/OneShot/request"] = AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE
  5. # Abort child animation connected to "shot" port.
  6. animation_tree.set("parameters/OneShot/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_ABORT)
  7. # Alternative syntax (same result as above).
  8. animation_tree["parameters/OneShot/request"] = AnimationNodeOneShot.ONE_SHOT_REQUEST_ABORT
  9. # Get current state (read-only).
  10. animation_tree.get("parameters/OneShot/active"))
  11. # Alternative syntax (same result as above).
  12. animation_tree["parameters/OneShot/active"]
  1. // Play child animation connected to "shot" port.
  2. animationTree.Set("parameters/OneShot/request", (int)AnimationNodeOneShot.OneShotRequest.Fire);
  3. // Abort child animation connected to "shot" port.
  4. animationTree.Set("parameters/OneShot/request", (int)AnimationNodeOneShot.OneShotRequest.Abort);
  5. // Get current state (read-only).
  6. animationTree.Get("parameters/OneShot/active");

时间缩放

这个节点可以用来使寻找命令发生在动画图像的任何子代上。使用这个节点类型可以从 AnimationNodeBlendTree 中的开始或某个位置播放 Animation

在设置时间和改变动画播放后,寻找节点通过设置其 seek_position 值为 -1.0,在下一个进程帧自动进入睡眠模式。

GDScriptC#

  1. # Play child animation from the start.
  2. animation_tree.set("parameters/TimeSeek/seek_request", 0.0)
  3. # Alternative syntax (same result as above).
  4. animation_tree["parameters/TimeSeek/seek_request"] = 0.0
  5. # Play child animation from 12 second timestamp.
  6. animation_tree.set("parameters/TimeSeek/seek_request", 12.0)
  7. # Alternative syntax (same result as above).
  8. animation_tree["parameters/TimeSeek/seek_request"] = 12.0
  1. // Play child animation from the start.
  2. animationTree.Set("parameters/TimeSeek/seek_request", 0.0);
  3. // Play child animation from 12 second timestamp.
  4. animationTree.Set("parameters/TimeSeek/seek_request", 12.0);

时间缩放

允许通过 scale 参数缩放连接到 in 输入的动画速度(或使其反转)。 将 scale 设置为0会暂停动画。

转换

非常简单的状态机(当你不想使用 StateMachine 节点时)。动画可以连接到输出,过渡时间可以指定。在设置请求和更改动画播放后,过渡节点会通过将其 transition_request 值设置为空字符串 (""),在下一个进程帧自动清除请求。

GDScriptC#

  1. # Play child animation connected to "state_2" port.
  2. animation_tree.set("parameters/Transition/transition_request", "state_2")
  3. # Alternative syntax (same result as above).
  4. animation_tree["parameters/Transition/transition_request"] = "state_2"
  5. # Get current state name (read-only).
  6. animation_tree.get("parameters/Transition/current_state")
  7. # Alternative syntax (same result as above).
  8. animation_tree["parameters/Transition/current_state"]
  9. # Get current state index (read-only).
  10. animation_tree.get("parameters/Transition/current_index"))
  11. # Alternative syntax (same result as above).
  12. animation_tree["parameters/Transition/current_index"]
  1. // Play child animation connected to "state_2" port.
  2. animationTree.Set("parameters/Transition/transition_request", "state_2");
  3. // Get current state name (read-only).
  4. animationTree.Get("parameters/Transition/current_state");
  5. // Get current state index (read-only).
  6. animationTree.Get("parameters/Transition/current_index");

二维混合空间

BlendSpace2D 是一个在二维空间进行高级混合的节点. 将点添加到一个二维空间, 然后可以控制位置来确定混合:

../../_images/animtree7.gif

可以控制X和Y的范围(为方便起见, 还可以标记它们). 默认情况下, 可以在任何位置放置点(只需右键单击坐标系统或使用 添加点 按钮)将自动生成德洛内三角形.

../../_images/animtree8.gif

也可以通过禁用 自动三角形 选项来手动绘制三角形, 虽然基本上没必要这么做:

../../_images/animtree9.png

最后, 可能会更改混合模式. 默认情况下, 混合是通过在最近的三角形内插点来实现的. 当处理二维动画(逐帧)时, 你可能希望切换到 离散 模式. 此外, 如果你想在离散动画之间切换时保持当前播放位置, 请使用 进位 模式. 此模式可在 混合 菜单中更改:

../../_images/animtree10.png

一维混合空间

这类似于二维混合空间, 但在一维空间中(所以不需要三角形).

状态机

这个节点是一个状态机,根节点都是状态。根节点可以创建并通过线路连接。状态通过转换连接,它们是具有特殊性质的连接。转换是单向的,但是可以用两个来达到双向连接。

../../_images/animtree11.gif

有多种类型的转换:

../../_images/animtree12.png

  • Immediate(立即):将立即切换到下一个状态。当前状态将结束,并与新状态的开头相混合。

  • Sync(同步):立即切换到下一个状态,但会将新状态快进并到旧状态的播放位置。

  • At End(末尾):将等待当前状态播放结束,然后切换到下一个状态动画的开头。

过渡也有一些属性。单击任何过渡,它就会显示在“检查器”面板中:

../../_images/animtree13.png

  • Switch Mode(切换模式)为过渡类型(见上文),可以在此处创建后修改。

  • Auto Advance(自动前进)当达到此状态时将自动开启转换。最适合 At End 切换模式。

  • Advance Condition(前进条件)会在条件成立时打开自动前进。这是一个可以用变量名填充的自定义文本字段。可以从代码中修改变量(稍后将对此进行详细介绍)。

  • Xfade Time(叠化时间)是在这个状态和下一个状态之间交叉渐变的时间。

  • Priority(优先级)与代码中的 travel() 函数一起使用(后述)。当从一个状态到另一个状态时,会优先使用优先级较低的过渡。

  • Disabled(禁用)允许禁用此转换(它不会在行程或自动前进期间使用)。

为了更好的混合

在 Godot 4.0+ 中,为了使混合结果具有确定性(结果可复现且始终一致),混合属性值必须具有特定的初始值。例如,在要混合两个动画的情况下,如果一个动画具有属性轨道而另一个动画没有,则计算混合动画时,要好像后一个动画(即本来没有属性轨道的那个)具有初始值的属性轨道一样去处理。

当使用 Skeleton3D 骨骼的 Position/Rotation/Scale 3D 轨道时,初始值为 Bone Rest(骨骼放松姿势)。对于其他属性而言,初始值是 0 ,并且如果轨道出现在 RESET 动画中,那么则使用它第一个关键帧的值。

例如,下面的 AnimationPlayer 有两个动画,但其中一个缺少 Position 的属性轨道。

../../_images/blending1.webp

这意味着缺少该 Position 的动画会将这些 Position 视为 Vector2(0, 0)

../../_images/blending2.webp

可以通过将 Position 的 Property 轨道作为初始值添加到 RESET 动画中来解决这个问题。

../../_images/blending3.webp ../../_images/blending4.webp

备注

请注意, RESET 动画的存在是为了在最初加载对象时定义默认姿势。它被假定只有一帧,并且不应使用时间轴进行播放。

另请记住,将“插值类型”设置为“线性角”或“三次角”的“Rotation 3D 轨道”和用于 2D 旋转的“属性”轨道,将阻止从初始值旋转超过 180 度的操作作为混合动画。

这种限制对于 Skeleton3D 非常有用,可以防止骨骼在混合动画时穿透身体。因此,Skeleton3D 的 Bone Rest (骨骼放松姿势)值应尽可能接近可移动范围的中点。 这意味着人形模型最好以 T-pose 导入

../../_images/blending5.webp

你可以看到,优先考虑从 Bone Rest 出发的最短旋转路径,而不是动画之间的最短旋转路径。

如果需要通过混合动画将 Skeleton3D 本身旋转 180 度以上,则可以使用 Root Motion。

根骨骼运动

处理 3D 动画时,一种流行的技术是动画师利用根骨骼为其余部分骨骼制作运动动画。这样处于动画角色的脚步就能够与下方的地板相匹配,并且还能够实现过场动画中与物体的精确交互。

在 Godot 中回放动画时,可以将这根骨骼选作根运动轨道。这会在视觉上取消这根骨骼的变换(在原地播放动画)。

../../_images/animtree14.png

这样做以后,可以通过 AnimationTree API 获取实际的变换:

GDScriptC#

  1. # Get the motion delta.
  2. animation_tree.get_root_motion_position()
  3. animation_tree.get_root_motion_rotation()
  4. animation_tree.get_root_motion_scale()
  5. # Get the actual blended value of the animation.
  6. animation_tree.get_root_motion_position_accumulator()
  7. animation_tree.get_root_motion_rotation_accumulator()
  8. animation_tree.get_root_motion_scale_accumulator()
  1. // Get the motion delta.
  2. animationTree.GetRootMotionPosition();
  3. animationTree.GetRootMotionRotation();
  4. animationTree.GetRootMotionScale();
  5. // Get the actual blended value of the animation.
  6. animationTree.GetRootMotionPositionAccumulator();
  7. animationTree.GetRootMotionRotationAccumulator();
  8. animationTree.GetRootMotionScaleAccumulator();

可以将这些值提供给 CharacterBody3D.move_and_slide 等函数,用来控制角色的移动。

还有一个名为 RootMotionView 的工具节点,可以放置在场景中充当角色和动画的自定义地板(这个节点默认在游戏期间禁用)。

../../_images/animtree15.gif

使用代码控制

创建树和预览之后,就只剩下一个问题:“这些东西怎么使用代码来控制?”。

要注意动画节点就是资源,因此他们会在所有使用他们的实例之间共享。直接修改节点中的值,将会影响到场景中所有使用这个 AnimationTree 的实例。通常是不希望这样的,不过也有一些不错的用法,比如你可以复制粘贴你的动画树的一部分,或者在不同的动画树中复用具有复杂布局的节点(例如状态机和混合树)。

实际的动画数据包含在 AnimationTree 节点中, 并通过属性访问. 检查 AnimationTree 节点的 “参数” 部分, 查看所有可以实时修改的参数:

../../_images/animtree16.png

这很方便, 因为它可以通过 AnimationPlayer 获得动画效果, 甚至是 AnimationTree 本身, 允许实现非常复杂的动画逻辑.

想要通过代码修改这些值, 必须获得该属性的路径. 这是很容易做到的, 把鼠标悬停在任何参数:

../../_images/animtree17.png

允许设置或读取它们:

GDScriptC#

  1. animation_tree.set("parameters/eye_blend/blend_amount", 1.0)
  2. # Simpler alternative form:
  3. animation_tree["parameters/eye_blend/blend_amount"] = 1.0
  1. animationTree.Set("parameters/eye_blend/blend_amount", 1.0);

状态机行程

Godot 的 StateMachine 实现提供了很多不错的功能,其中之一就是“行程”(Travel)的能力。可以向图发出指令,让其从当前状态转到另一个状态,所有的中间状态都会被访问到。这是通过 A* 算法实现的。如果当前状态和目的状态之间不存在任何可达的过渡路径集,图就会立即传送到目的状态。

要使用行程能力, 你应该首先从 AnimationTree 节点中检索 AnimationNodeStateMachinePlayback 对象(其被导出为一个属性).

GDScriptC#

  1. var state_machine = animation_tree["parameters/playback"]
  1. AnimationNodeStateMachinePlayback stateMachine = (AnimationNodeStateMachinePlayback)animationTree.Get("parameters/playback");

一旦检索到, 可以调用它提供的许多函数之一:

GDScriptC#

  1. state_machine.travel("SomeState")
  1. stateMachine.Travel("SomeState");

状态机必须正在运行才能使用行程能力。确保调用 start() 或选择一个节点以在加载时自动播放

../../_images/animtree18.png