如何使用代码在AnimationTree中开始和旅行?

How to start & travel in AnimationTree using code?

我正在尝试通过在 AnimationTree 中的节点之间移动来为可扩展的员工制作动画,就像这样:

...
tool

export(String,"small", "mid", "full") var staff_mode = "small" setget set_staff_mode;

func set_staff_mode(new_val):
    
    var ani_state;
    if(self.has_node("/path/to/AnimationTree")):
        ani_state=self.get_node("/path/to/AnimationTree")["parameters/playback"];
        ani_state.start(staff_mode);
        print(ani_state.is_playing());
        ani_state.travel(new_val);
        ani_state.stop();
        
    staff_mode=new_val;

我还没有对 small 应用自动播放,因为我不想在五线谱上播放循环动画
(它只展开或压缩,没有空闲动画)

但由于某种原因它给出了错误:

Can't travel to 'full' if state machine is not playing. Maybe you need to enable Autoplay on Load for one of the nodes in your state machine or call .start() first?

编辑: 我忘记说了,但是我的员工没有任何空闲动画,所以我需要在转换完成后停止动画。

小号、中号和全号
(都是静态模式的法杖,具体取决于游戏法杖应该延长多少)
都是 0.1 秒的单帧动画,我应用了 0.2 秒的 Xfade 时间来显示过渡

我只需要从现有的动画状态转换到另一个然后停止


新答案

显然,旧答案的解决方案不适用于非常短的动画。根据我的口味,变通办法似乎开始变得过多。因此,作为替代方案,让我们摆脱 AnimationTree 并直接使用 AnimationPlayer。为此,我们将:

  1. 将动画持续时间增加到足以满足“淡入淡出”时间(例如 0.2 秒)。
  2. 将“淡入淡出”时间放入“Cross-Animation 混合时间”。对于每个动画,select 在动画面板上,然后从动画菜单中 select“编辑过渡...”,打开“Cross-Animation 混合时间”,您可以在其中指定过渡时间其他动画(例如 0 到自身,0.1 到“相邻”动画,等等)。

现在我们可以简单地让 AnimationPlayer 播放,像这样:

tool
extends Node2D

enum Modes {full, mid, small}
export(Modes) var staff_mode setget set_staff_mode

func set_staff_mode(new_val:int) -> void:
    if staff_mode == new_val:
        return

    if not is_inside_tree():
        return

    var animation_player := get_node("AnimationPlayer") as AnimationPlayer
    if not is_instance_valid(animation_player):
        return

    var target_animation:String = Modes.keys()[new_val]
    animation_player.play(target_animation)
    yield(animation_player, "animation_finished")
    staff_mode = new_val
    property_list_changed_notify()

我选择使用枚举,因为这也允许我在动画之间“旅行”。这个想法是我们将创建一个 for 循环,我们按顺序调用动画。像这样:

tool
extends Node2D

enum Modes {full, mid, small}
export(Modes) var staff_mode:int setget set_staff_mode

func set_staff_mode(new_val:int) -> void:
    if staff_mode == new_val:
        return

    if not is_inside_tree():
        return

    var animation_player := get_node("AnimationPlayer") as AnimationPlayer
    if not is_instance_valid(animation_player):
        return

    var old_val := staff_mode
    staff_mode = new_val
    var travel_direction = sign(new_val - old_val)
    for mode in range(old_val, new_val + travel_direction, travel_direction):
        var target_animation:String = Modes.keys()[mode]
        animation_player.play(target_animation)
        yield(animation_player, "animation_finished")

我也决定早点设置staff_mode这样我就可以避免property_list_changed_notify.

并发调用可能会导致动画提前停止,因为调用 play 会停止当前正在播放的动画以播放新动画。但是,我认为等待当前动画结束是不正确的。还有这么短的动画,应该问题不大。


使用 Tween 的版本

使用 Tween 会给你更好的控制,但它也需要更多的工作,因为我们要用代码对动画进行编码……我将用 interpolate_property 来做。值得庆幸的是,这是一个相当简单的动画,因此可以在不使代码太长的情况下进行管理。

当然需要添加一个Tween节点。我们不会使用 AnimationPlayerAnimationTreeTween 将处理插值(您甚至可以通过添加 interpolate_property 的可选参数来指定如何进行插值,我不会在此处传递)。

这是代码:

tool
extends Node2D

enum Modes {full, mid, small}
export(Modes) var staff_mode:int setget set_staff_mode

func set_staff_mode(new_val:int) -> void:
    if staff_mode == new_val:
        return

    if not is_inside_tree():
        return

    var tween := get_node("Tween") as Tween
    if not is_instance_valid(tween):
        return

    var old_val := staff_mode
    staff_mode = new_val
    var travel_direction = sign(new_val - old_val)
    for mode in range(old_val, new_val + travel_direction, travel_direction):
        match mode:
            Modes.full:
                tween.interpolate_property($"1", "position", $"1".position, Vector2.ZERO, 0.2)
                tween.interpolate_property($"1/2", "position", $"1/2".position, Vector2(0, -35), 0.2)
                tween.interpolate_property($"1/2/3", "position", $"1/2/3".position, Vector2(0, -34), 0.2)
            Modes.mid:
                tween.interpolate_property($"1", "position", $"1".position, Vector2.ZERO, 0.2)
                tween.interpolate_property($"1/2", "position", $"1/2".position, Vector2(0, -35), 0.2)
                tween.interpolate_property($"1/2/3", "position", $"1/2/3".position, Vector2.ZERO, 0.2)
            Modes.small:
                tween.interpolate_property($"1", "position", $"1".position, Vector2.ZERO, 0.2)
                tween.interpolate_property($"1/2", "position", $"1/2".position, Vector2.ZERO, 0.2)
                tween.interpolate_property($"1/2/3", "position", $"1/2/3".position, Vector2.ZERO, 0.2)

        tween.start()
        yield(tween, "tween_all_completed")

你在这里看到的是我已经对源代码中 AnimationPlayer 的轨道的值进行了编码。使用 Tween 我可以告诉它从轨道具有的任何值到每个状态的目标位置进行插值。

我不知道与AnimationPlayer相比,它的性能是更好还是更差。


旧答案

好吧,这个问题有两个方面:

  • travel应该是播放动画,所以不是瞬时的。因此,如果您调用 stop 它将无法旅行,并且您会收到错误消息。

  • 啊,但是你也不能叫start然后背靠背旅行。您需要等待动画开始。


我将从不从一个状态转到相同状态开始:

func set_staff_mode(new_val:String) -> void:
    if staff_mode == new_val:
        return

我们将需要获取 AnimationTree,因此我们需要在场景树中进行 ge。这里我使用yield所以方法returns,Godot在得到"tree_entered"信号后恢复执行:

    if not is_inside_tree():
        yield(self, "tree_entered")

yield 的缺点是,如果 Node 在您收到信号之前空闲,它可能会导致错误。因此,如果您不想使用 yield,我们可以这样做:

    if not is_inside_tree():
        # warning-ignore:return_value_discarded
        connect("tree_entered", self, "set_staff_mode", [new_val], CONNECT_ONESHOT)
        return

此处CONNECT_ONESHOT确保此信号自动断开。此外,Godot 确保在释放 Node 时断开所有信号,因此这不会出现与 yield 相同的问题。但是,与 yield 不同的是,它不会在方法中间开始,而是会再次调用该方法。


好的,我们得到 AnimationTree:

var animation_tree := get_node("/path/to/AnimationTree")
if not is_instance_valid(animation_tree):
    return

并得到 AnimationNodeStateMachinePlayback:

var ani_state:AnimationNodeStateMachinePlayback = animation_tree.get("parameters/playback")

现在,如果它没有播放,我们需要让它播放:

    if not ani_state.is_playing():
        ani_state.start(new_val)

现在的问题是:我们需要等待动画开始。

代替更好的解决方案,我们将为其合并:

        while not ani_state.is_playing():
            yield(get_tree(), "idle_frame")

之前我建议获取 AnimationPlayer 以便我们可以等待 "animation_started",但那行不通。


最后,既然我们知道它正在播放,我们可以使用 travel,并更新状态:

    ani_state.travel(new_val)
    staff_mode = new_val

不要调用stop


你可能还想在最后调用 property_list_changed_notify(),所以 Godot 读取 staff_mode 的新值,它可能没有注册,因为我们没有立即更改它(而是我们在改变它之前屈服了)。 我想您也可以更早地更改值,在任何 yield.

之前

顺便说一下,如果您希望 mid 动画在 travel 中完成,请将 AnimationTreemid 中的连接从“Immidiate”更改为"到"AtEnd"。


关于等待旅行结束和停止的附录

我们可以像等待 AnimationNodeStateMachinePlayback 开始播放一样以类似的方式旋转等待。这个时候我们需要池两个薄小号:

  • 动画的当前状态是什么。
  • 该动画的播放位置是什么。

只要动画不是最终状态,只要还没有到达那个动画的结尾,我们就让一帧通过,再检查一遍。像这样:

    while (
        ani_state.get_current_node() != new_val
        or ani_state.get_current_play_position() < ani_state.get_current_length()
    ):
        yield(get_tree(), "idle_frame")

那你可以打电话给stop.


此外,我将添加 is_playing 的检查。原因是这段代码正在等待 AnimationTree 完成我们告诉它的状态......但是如果你在它完成之前再次调用旅行,它会去新的目的地,因此可能永远不会达到我们预期的状态,这导致旋转永远等待。

并且由于它可能没有达到我们预期的状态,所以我决定查询最终状态而不是将 staff_mode 设置为 new_val。那部分代码现在看起来像这样:

    ani_state.travel(new_val)
    while (
        ani_state.is_playing() and (
            ani_state.get_current_node() != new_val
            or ani_state.get_current_play_position()
               < ani_state.get_current_length()
        )
    ):
        yield(get_tree(), "idle_frame")

    ani_state.stop()
    staff_mode = ani_state.get_current_node()
    property_list_changed_notify()