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

发布于 2025-01-14 03:49:27 字数 1081 浏览 3 评论 0原文

我正在尝试通过在 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 应用自动播放,因为我不想要一个工作人员上的循环动画
(它只扩展或压缩,没有空闲动画)

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

如果状态机未运行,则无法进入“满”状态。也许你 需要为您所在州的节点之一启用加载时自动播放 机器还是先调用.start()?

编辑: 我忘了提及,但我的员工没有任何空闲动画,因此我需要在过渡完成后停止动画。

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

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

I'm trying to animate an expandable staff by travel between nodes in AnimationTree Like this:

...
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;

enter image description here

I haven't applied autoplay to small because I don't want a looping animation on the staff
(it only expands or compresses, no idle animation)

but for some reason it gives the error:

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?

Edit:
I forgot to mention but I don't have any idle animation for my staff so I need to stop the animation after the transition is complete.

small, mid & full
(all of them are static modes of the staff depending upon the game how much the staff should extend)
are all 0.1sec single frame animations and I applied Xfade Time of 0.2 secs to show the transition

I simply need to transition from an existing animation state to another and then stop

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

傻比既视感 2025-01-21 03:49:27

新答案

显然旧答案的解决方案不适用于非常短的动画。对于我的口味来说,这些变通办法似乎开销太大了。因此,作为替代方案,让我们摆脱 AnimationTree 并直接使用 AnimationPlayer。为此,我们将:

  1. 增加动画持续时间,使其足够长以适应“交叉淡入淡出”时间(例如 0.2 秒)。
  2. 将“交叉淡入淡出”时间放入“交叉动画混合时间”中。对于每个动画,在“动画”面板上选择它,然后从动画菜单中选择“编辑过渡...”,这将打开“跨动画混合时间”,您可以在其中指定其他动画的过渡时间(例如 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 相比,它的性能是更好还是更差。


旧答案

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

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


  • 啊,但是你也不能调用 start 并连续旅行。您需要等待动画开始。


我首先不会从一种状态转到相同的状态:

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

我们需要获取 AnimationTree,因此我们需要进入场景树。这里我使用 yield 所以方法返回,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动画在< code>travel,将 AnimationTree 中从 mid 发出的连接从“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 完成我们告诉它的状态...但是如果您在完成之前再次调用 Travel,它将前往新的目的地,因此可能永远不会到达我们期望的状态,这会导致自旋永远等待。

由于它可能没有达到我们预期的状态,我决定查询最终状态,而不是将 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()

New Answer

Apparently the solution on the old answer does not work for very short animations. And the workarounds begin to seem to much overhead for my taste. So, as alternative, let us get rid of the AnimationTree and work directly with AnimationPlayer. To do this, we will:

  1. Increase the animation duration to be long enough for the "cross fade" time (e.g. 0.2 seconds).
  2. Put the "cross fade" time in "Cross-Animation Blend Times". For each animation, select it on the Animation panel and then select "Edit Transition…" from the animation menu, that opens the "Cross-Animation Blend Times" where you can specify the transition time to the other animations (e.g. 0 to itself, 0.1 to "adjacent" animation, and so on).

Now we can simply ask the AnimationPlayer to play, something like this:

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()

I have opted to use an enum, because this will also allow me to "travel" between the animations. The idea is that we will make a for loop where we call the animations in order. Like this:

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")

I have also decided to set staff_mode early so I can avoid property_list_changed_notify.

Concurrent calls may result in animation stopping early, since calling play stops the currently playing animation to play the new one. However, I don't think waiting for the current animation to end is correct. Also with so short animations, it should not be a problem.


Version using Tween

Using Tween will give you finer control, but it is also more work, because we are going to encode the animations in code… Which I will be doing with interpolate_property. Thankfully this is a fairly simple animation, so can manage without making the code too long.

Of course, you need to add a Tween node. We will not use AnimationPlayer nor AnimationTree. Tween will handle the interpolations (and you can even specify how to do the interpolations by adding the optional parameters of interpolate_property, which I'm not passing here).

This is the code:

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(
quot;1", "position", 
quot;1".position, Vector2.ZERO, 0.2)
                tween.interpolate_property(
quot;1/2", "position", 
quot;1/2".position, Vector2(0, -35), 0.2)
                tween.interpolate_property(
quot;1/2/3", "position", 
quot;1/2/3".position, Vector2(0, -34), 0.2)
            Modes.mid:
                tween.interpolate_property(
quot;1", "position", 
quot;1".position, Vector2.ZERO, 0.2)
                tween.interpolate_property(
quot;1/2", "position", 
quot;1/2".position, Vector2(0, -35), 0.2)
                tween.interpolate_property(
quot;1/2/3", "position", 
quot;1/2/3".position, Vector2.ZERO, 0.2)
            Modes.small:
                tween.interpolate_property(
quot;1", "position", 
quot;1".position, Vector2.ZERO, 0.2)
                tween.interpolate_property(
quot;1/2", "position", 
quot;1/2".position, Vector2.ZERO, 0.2)
                tween.interpolate_property(
quot;1/2/3", "position", 
quot;1/2/3".position, Vector2.ZERO, 0.2)

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

What you can see here is that I have encoded the values from the tracks of from the AnimationPlayer in the source code. Using Tween I get to tell it to interpolate from whatever value the track has to the target position of each state.

I don't know if this performs better or worse compared to AnimationPlayer.


Old Answer

Alright, there are two sides to this problem:

  • travel is supposed to play the animation, so it is not instantaneous. Thus, if you call stop it will not be able to travel, and you get the error message you got.

  • Ah, but you cannot call start and travel back to back either. You need to wait the animation to start.


I'll start by not going from one state to the same:

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

We are going to need to get the AnimationTree, so we need to ge in the scene tree. Here I use yield so the method returns, and Godot resumes the execute after it gets the "tree_entered" signal:

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

The drawback of yield, is that it can cause an error if the Node is free before you get the signal. Thus, if you prefer to not use yield, we can do this instead:

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

Here CONNECT_ONESHOT ensures this signal is automatically disconnected. Also, Godot makes sure to disconnect any signals when freeing a Node so this does not have the same issue as yield. However, unlike yield it will not start in the middle of the method, instead it will call the method again.


Alright, we get the AnimationTree:

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

And get the AnimationNodeStateMachinePlayback:

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

Now, if it is not playing, we need to make it playing:

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

And now the problem: we need to wait for the animation to start.

In lieu of a better solution, we going to pool for it:

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

Previously I was suggesting to get the AnimationPlayer so we can wait for "animation_started", but that does not work.


Finally, now that we know it is playing, we can use travel, and update the state:

    ani_state.travel(new_val)
    staff_mode = new_val

Don't call stop.


You might also want to call property_list_changed_notify() at the end, so Godot reads the new value of staff_mode, which it might not have registered because we didn't change it right away (instead we yielded before changing it). I suppose you could alternatively change the value earlier, before any yield.


By the way, if you want the mid animation to complete in the travel, change the connections going out of mid in the AnimationTree from "Immidiate" to "AtEnd".


Addendum on waiting for travel to end and stop

We can spin wait in a similar fashion as we did to wait for the AnimationNodeStateMachinePlayback to start playing. This time we need to pool two things:

  • What is the current state of the animation.
  • What is the playback position on that animation.

As long as the animation is not in the final state and as long as it has not reached the end of that animation, we let one frame pass and check again. Like this:

    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")

Then you can call stop.


Furthermore, I'll add a check for is_playing. The reason is that this code is waiting for the AnimationTree to complete the state we told it to… But if you call travel again before it finished, it will go to new destination, and thus might never reach the state we expected, which result in spin waiting for ever.

And since it might not have arrived to the state we expected, I decided to query the final state instead of setting staff_mode to new_val. That part of the code now looks like this:

    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()
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文