Skip to content

AnimationPlayer and AnimationTree

AnimationTree in Godot can be used to control and transition between animations.

As an example scenario, suppose you buy a spritesheet like this. The character has an idle animation, they can jump, attack, walk, run, etc. I used a subset of these and ended up with this AnimationTree:

  • Cleaning up the trees: you should be able to have “sub” trees. I haven’t done this myself yet, but it’s worth noting for the future.
  • To change an AnimationPlayer’s FPS after already laying out the frames, the easiest thing to do is select all of the keyframes, then click EditScale Selection… E.g. if you have frames laid out every 0.1 seconds (which would be 10 FPS) and want them to be every 0.05 seconds (20 FPS), you would set a scale factor of 0.5.
    • You could also add a BlendTree in an AnimationTree and have your animation going to a TimeScale node and then eventually into Output.
  • Animating sprites
    • IMO, it’s best to use AnimatedSprite2D only for very simple scenarios, e.g. 4-way walking animations that you switch between from code. When you have more complexity than that, you would use an AnimationTree with an AnimationPlayer and a Sprite2D (not an AnimatedSprite2D—my rationale against using them is that you wouldn’t want to confuse someone else in your codebase who thinks the animations are coming from AnimatedSprite2D instead of AnimationPlayer and AnimationTree).
  • AnimationTree lets you use conditions in a very basic way or expressions in a more advanced way. If using expressions, you have to manifest the variables yourself (they can be private properties in C#) and then only use expressions (I believe). As a result, I don’t think conditions have much of a purpose to exist given that expressions exist (they’re probably only for very simple scenarios).
  • Suppose you are animating a cut-scene and you want to make an AnimatedSprite2D walk right, then walk up, then walk left (i.e. it would use three animations). You could set the animation property from an AnimationPlayer or even call the play method, but neither of those would show any change while in the editor. Instead, you would have to change the frame of an AnimatedSprite2D. If you have an AnimationPlayer and/or an AnimationTree set up already, then you can have one AnimationPlayer play the animations from another via current_animation (you may need to click the AnimationTree so that it doesn’t play out its own animations).

Adding animations when using AnimationPlayer and AnimationTree

Section titled “Adding animations when using AnimationPlayer and AnimationTree”

Note: it may be better to use this addon.

  • This isn’t super obvious because the tree sort of “fights” you as you try changing things.
    • Click the AnimationTree in the scene hierarchy
    • Make sure to select “Animation” at the very bottom, which will show you a dummy copy of the AnimationPlayer.
    • Click the ⏹️ button to stop any animations from playing.
    • Click “Animation” in the dummy AnimationPlayer and add a new animation.
    • Click the Sprite2D that you want to animate. The AnimationPlayer will remain open in the bottom panel.
    • Set its texture
    • Under Animation in the Inspector, set how many columns and rows you have (Hframes and Vframes).
    • Click the 🔑 icon next to the following and in this order:
      • Hframes
      • Vframes
      • Texture
      • Frame
    • Set the “Nearest FPS” at the bottom of the AnimationPlayer based on how far apart you want the frames. E.g. at 10 FPS, you would set this to 0.1.
    • Set the Animation length at the upper right of the AnimationPlayer to be the total duration of the animation. If you have 16 frames and want them to run 0.1s apart, you would set this to 1.6.
    • Click the key next to “Frame” until you have all of your frames in the AnimationPlayer

Idiomatic usage of AnimationNodeBlendSpace2D

Section titled “Idiomatic usage of AnimationNodeBlendSpace2D”

Here’s a pretty common scenario in a lot of games: you have a spritesheet with animations for three directions representing many different actions (e.g. idle, walking, attacking, etc.). You want to pick the right animation depending on which direction you’re facing, and you want the fourth direction to be a horizontal flip of an existing one.

This setup worked pretty well for me:

  • Scene tree:
    • Sprite2D - this is what we’ll animate
    • AnimationPlayer - this contains animations for the sprite
    • AnimationTree - this controls the AnimationPlayer for the Sprite2D
      • AnimationNodeStateMachine - (note: this is within the AnimationTree, not in the scene hierarchy itself) - this will have high-level states like idle and walk with transitions between them.
        • AnimationNodeBlendSpace2D - add one of these for each high-level state

Within the AnimationNodeBlendSpace2D, right-click the background and add animations as shown:

Then, through code, do something like this:

using System;
using Godot;
namespace Game.SubGames.FarmingGame;
public partial class FarmingPlayer : CharacterBody2D
{
private AnimationTree _animationTree = null!;
private AnimationNodeStateMachinePlayback _playback = null!;
private Vector2 _priorCardinalDirection = Vector2.Down;
private Sprite2D _sprite = null!;
public override void _Ready()
{
_sprite = GetNode<Sprite2D>("%Sprite");
_animationTree = GetNode<AnimationTree>("%AnimationTree");
_playback = (AnimationNodeStateMachinePlayback)_animationTree.Get("parameters/playback");
}
public override void _PhysicsProcess(double delta)
{
var direction = Input.GetVector(
"ui_left",
"ui_right",
"ui_up",
"ui_down"
);
var cardinalDirection = _priorCardinalDirection;
if (!direction.IsZeroApprox())
{
if (MathF.Abs(direction.X) > MathF.Abs(direction.Y))
{
cardinalDirection = new Vector2(direction.X, 0);
}
else
{
cardinalDirection = new Vector2(0, direction.Y);
}
_priorCardinalDirection = cardinalDirection;
const float speed = 40f;
Velocity = cardinalDirection * speed;
}
else
{
Velocity = Vector2.Zero;
}
var isFacingLeft = cardinalDirection.X < 0;
_sprite.FlipH = isFacingLeft;
var facing = isFacingLeft ? Vector2.Right : cardinalDirection;
var isWalking = !Velocity.IsZeroApprox();
_playback.Travel(isWalking ? "walk" : "idle");
_animationTree.Set($"parameters/{_playback.GetCurrentNode()}/blend_position", facing);
MoveAndSlide();
}
}

set_frame: Index p_frame = 6 is out of bounds (vframes * hframes = 6).

Section titled “set_frame: Index p_frame = 6 is out of bounds (vframes * hframes = 6).”

If you get this error:

E 0:00:20:0247 set_frame: Index p_frame = 6 is out of bounds (vframes * hframes = 6).

…make sure to put “frame” at the bottom of the animation player (reference):

E.g.:

W 0:00:02:820 _transition_to_next_recursive: AnimationNodeStateMachinePlayback: parameters/playback has detected one or more looped transitions in a single frame and aborted to prevent an infinite loop. You may need to check the transition settings.
<C++ Source> scene/animation/animation\_node\_state\_machine.cpp:938 @ _transition_to_next_recursive()

This happens when you have something like this: Pasted image 20260408155049.png

The problem is that both states have their conditions met to go to the next state, which would mean that the transitions would never stop. This can also happen when you have more than two states as long as there’s any infinite loop.

To figure out which transition is causing this, you can change the transition’s Advance mode (advance_mode) to Disabled, and if it stops happening, then it was probably that one. 😛 Then, to fix it, you either need to change your condition or change your code.

For example, in the picture above, I originally had a condition of IsAirDashing to go to the “air-dashing” state and IsJumping to go to the “jumping” state. To fix this, I changed one condition to !IsAirDashing && IsJumping.

In another case, I changed the code to prevent both booleans from being true at the same time.

GetCurrentNode doesn’t update synchronously

Section titled “GetCurrentNode doesn’t update synchronously”
AnimationNodeStateMachinePlayback playback = (AnimationNodeStateMachinePlayback)_animationTree.Get("parameters/playback");
// Suppose playback is in the "idle" state
playback.Travel("walking");
// At this point, playback.GetCurrentNode() will still say "idle" even though you just changed it

To work around this behavior, you could just keep track of what should be the current node yourself. Alternatively, you could wait for a frame.