Free Tutorial · Godot 4 Animation

Godot 4 Animation Tutorial — AnimationPlayer, AnimationTree
& Blending

SScriptSnap/March 2026/13 min read

Animation is what separates a playable prototype from a game that feels good. Godot 4 ships with a complete animation toolkit — AnimationPlayer for keyframe editing, AnimationTree for state-machine blending, and first-class GDScript hooks so you can drive everything from code.

In this Godot 4 animation tutorial, you'll learn how to animate sprites, properties, and whole characters. We'll cover AnimationPlayer basics, keyframe workflows, AnimationTree state machines, blend spaces for smooth movement, and code-driven animation control. By the end you'll have 6+ copy-paste GDScript examples covering character movement blending, attack combos, and polished UI animations.

Whether you're rigging a 2D platformer or smoothing out a top-down RPG, this guide has you covered. Let's dive in.

Tutorial

AnimationPlayer Basics — Your Starting Point

Every animation in Godot starts with AnimationPlayer. Add one as a child of any node and it can animate any property on any sibling or descendant — position, scale, color, modulate, custom shader params, even exported variables.

The workflow is dead simple: create an Animation resource inside AnimationPlayer, add property tracks, set keyframes on the timeline, and press play. Godot interpolates the values between keys automatically.

Here's what makes Godot AnimationPlayer so powerful:

  • Animate ANY property on ANY node — position, rotation, modulate, shader uniforms, you name it
  • Method Call tracks — trigger functions at specific frames (perfect for SFX and particles)
  • Bezier tracks — fine-tune easing curves for cinematic motion
  • Audio tracks — sync sound effects directly to animation timelines
  • Multiple animations per player — switch between idle, walk, attack instantly

If you're coming from Unity, think of AnimationPlayer as the Animator + Animation Window combined into one node. If you used Godot 3, the API is mostly the same — with some nice quality-of-life improvements in Godot 4's editor.

Keyframe Animation — Sprite Sheets and Properties

For Godot sprite animation, the most common approach is animating the frame property of a Sprite2D or AnimatedSprite2D. With AnimationPlayer you get precise control over timing, looping, and which frame plays when.

Here's how to set up a basic sprite animation from code. This example creates a simple four-frame walk cycle and plays it:

player.gd — Basic Sprite Animation Setup
extends CharacterBody2D

@onready var anim_player: AnimationPlayer = $AnimationPlayer
@onready var sprite: Sprite2D = $Sprite2D

func _ready() -> void:
    # Play the "idle" animation on load
    anim_player.play("idle")

func _physics_process(delta: float) -> void:
    var direction := Input.get_axis("move_left", "move_right")
    velocity.x = direction * 200.0

    # Flip sprite based on direction
    if direction != 0:
        sprite.flip_h = direction < 0
        anim_player.play("walk")
    else:
        anim_player.play("idle")

    move_and_slide()

Notice we call anim_player.play() every frame — AnimationPlayer is smart enough to ignore the call if the same animation is already running. No need for an if anim_player.current_animation != "walk" guard.

Beyond sprite frames, AnimationPlayer can keyframe any property: a PointLight2D's energy for flickering torches, a ColorRect's color for screen flashes, or a shader_parameter for animated dissolves. Check out our Godot 4 Shader Tutorial for more on driving shaders with animations.

AnimationTree — State Machines for Complex Characters

AnimationPlayer.play() works great for simple cases, but as your character gains more states (idle, walk, run, jump, fall, attack, hurt, die…) you'll want smooth transitions and blending between them. That's where AnimationTree comes in.

Godot AnimationTree sits on top of AnimationPlayer. It reads the animations from the player but controls which one plays and how they blend. The main mode you'll use is the AnimationNodeStateMachine — a visual graph where each node is an animation and edges are transitions with conditions.

Setting up an AnimationTree state machine:

  • Add an AnimationTree node as a sibling of AnimationPlayer
  • Set the Tree Root to AnimationNodeStateMachine
  • Assign the AnimationPlayer in the Anim Player property
  • Add animation nodes for each state (idle, walk, run, jump, etc.)
  • Draw transitions between states and configure conditions
  • Set Active to true — the tree now controls playback

Transitions can be set to automatic (switch when the current animation ends), or manual (you call travel() from code). Each transition has a cross-fade time so animations blend smoothly instead of snapping. This approach pairs perfectly with a GDScript state machine pattern for game logic.

player.gd — Controlling AnimationTree State Machine
extends CharacterBody2D

@onready var anim_tree: AnimationTree = $AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback = \
    anim_tree["parameters/playback"]

func _physics_process(delta: float) -> void:
    var direction := Input.get_vector(
        "move_left", "move_right", "move_up", "move_down"
    )
    velocity = direction * 300.0
    move_and_slide()

    # Travel to the correct state
    if velocity.length() > 10.0:
        state_machine.travel("run")
    else:
        state_machine.travel("idle")

func _on_hit() -> void:
    # Interrupt any state and play hurt animation
    state_machine.travel("hurt")

func _on_attack_pressed() -> void:
    state_machine.travel("attack")

The travel() method finds the shortest path through the state graph and cross-fades through each transition. If a direct edge doesn't exist between the current state and the target, it will route through intermediate states automatically. For instant jumps (no blending), use start() instead.

Want to build clean, scalable state machines for your game logic too? Our State Machines micro-course teaches the same pattern used in professional Godot projects.

View Course →
📧

Want more Godot tricks?

Get our free GDScript Cheat Sheet with 20+ copy-paste snippets for signals, exports, state machines & more.

GDSJoin 500+ Godot devs · No spam · Unsubscribe anytime

Blend Spaces — Smooth Movement Animation

State machines handle discrete states well, but what about continuous blending? A top-down character walking in any direction needs to smoothly interpolate between up, down, left, and right walk animations. That's what BlendSpace2D does.

Godot animation blending with BlendSpace2D maps a 2D input vector (like your movement direction) to a set of animations placed on a 2D graph. As the input changes, the engine automatically blends between the nearest animations. For 1D blending (like walk-to-run based on speed), use BlendSpace1D.

Here's a common setup for 8-directional movement blending:

player.gd — BlendSpace2D for 8-Direction Movement
extends CharacterBody2D

@onready var anim_tree: AnimationTree = $AnimationTree

const SPEED := 200.0

func _physics_process(delta: float) -> void:
    var input_dir := Input.get_vector(
        "move_left", "move_right", "move_up", "move_down"
    )
    velocity = input_dir * SPEED
    move_and_slide()

    # Feed direction into BlendSpace2D
    # The parameter path matches your AnimationTree setup
    if input_dir != Vector2.ZERO:
        anim_tree["parameters/walk/blend_position"] = input_dir
        anim_tree["parameters/idle/blend_position"] = input_dir

    # Toggle between idle and walk blend spaces
    var state_machine: AnimationNodeStateMachinePlayback = \
        anim_tree["parameters/playback"]
    if velocity.length() > 10.0:
        state_machine.travel("walk")
    else:
        state_machine.travel("idle")

In the AnimationTree editor, you'd place your directional animations at coordinates on the BlendSpace2D grid: walk_right at (1, 0), walk_left at (-1, 0), walk_down at (0, 1), walk_up at (0, -1), and diagonal variants at the corners. The engine triangulates between the three nearest points and blends them in real time.

BlendSpace1D is simpler and great for speed-based blending: place idle at 0, walk at 0.5, and run at 1.0, then feed in your normalized speed. You get buttery-smooth transitions with zero code logic. If you want even more code-driven animation juice, check out our Godot 4 Tween Tutorial for layering tweens on top of AnimationPlayer.

Code-Driven Animations — GDScript Control

Sometimes you need frame-perfect control that the visual editor can't give you. AnimationPlayer exposes a full GDScript API for creating, queuing, seeking, and blending animations programmatically.

Key methods you'll use:

  • play(name) play an animation by name
  • play_backwards(name) play in reverse (great for retract animations)
  • queue(name) queue an animation to play after the current one finishes
  • stop() stop the current animation
  • seek(time, update) jump to a specific time in the animation
  • speed_scale speed up or slow down playback globally
  • animation_finished signal react when an animation completes

The animation_finished signal is especially useful. Connect it to chain animations, trigger game logic after an attack lands, or return to idle after a one-shot animation. You can also use the Signal Bus pattern to broadcast animation events globally.

Want the complete GDScript reference at your fingertips? Grab our free cheat sheet — 20+ copy-paste snippets for animations, signals, tweens, and more.

Get Cheat Sheet →

Practical Example — Attack Combo System

Let's build something real: a 3-hit attack combo. The player presses attack during the current swing to queue the next hit. If they're too slow, the combo resets. This is a classic pattern for action games and it shows off AnimationPlayer's queue system and signals beautifully.

player_combat.gd — 3-Hit Attack Combo
extends Node2D

@onready var anim_player: AnimationPlayer = $AnimationPlayer

var combo_step: int = 0
var can_combo: bool = false
var combo_window_timer: float = 0.0
const COMBO_WINDOW: float = 0.4  # seconds to input next hit

func _ready() -> void:
    anim_player.animation_finished.connect(_on_animation_finished)

func _process(delta: float) -> void:
    if combo_window_timer > 0.0:
        combo_window_timer -= delta
        if combo_window_timer <= 0.0:
            _reset_combo()

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("attack"):
        if combo_step == 0:
            # Start the combo
            combo_step = 1
            anim_player.play("attack_1")
        elif can_combo and combo_step < 3:
            # Chain the next hit
            can_combo = false
            combo_step += 1
            anim_player.play("attack_" + str(combo_step))

func _on_animation_finished(anim_name: String) -> void:
    if anim_name.begins_with("attack_"):
        if combo_step < 3:
            # Open the combo window
            can_combo = true
            combo_window_timer = COMBO_WINDOW
        else:
            _reset_combo()

func _reset_combo() -> void:
    combo_step = 0
    can_combo = false
    combo_window_timer = 0.0
    anim_player.play("idle")

The key insight: we use the animation_finished signal to open a short combo window. If the player presses attack within that window, we advance to the next animation. After the third hit (or if the window expires), we reset. Each attack_1, attack_2, and attack_3 animation would have Method Call tracks to spawn hitboxes, play SFX, and emit screen shake signals.

For a deeper dive into organizing game logic with signals, see our Signal Bus pattern guide.

Practical Example — Polished UI Animations

AnimationPlayer isn't just for gameplay — it's also the best tool for UI animations in Godot. Menu transitions, health bar pulses, inventory slot highlights — all of these benefit from keyframed animations rather than code-driven tweens because designers can tweak timing in the editor.

Here's a pattern for a reusable UI panel that slides in and fades out, controlled entirely from code:

ui_panel.gd — Slide-In Menu with AnimationPlayer
extends Control

@onready var anim_player: AnimationPlayer = $AnimationPlayer

func open_menu() -> void:
    visible = true
    anim_player.play("slide_in")
    # "slide_in" animates:
    #   - position.x: from 400 → 0 (slide from right)
    #   - modulate.a: from 0 → 1 (fade in)
    #   - scale: from 0.95 → 1.0 (subtle zoom)

func close_menu() -> void:
    anim_player.play_backwards("slide_in")
    await anim_player.animation_finished
    visible = false

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("ui_cancel") and visible:
        close_menu()

The trick here is play_backwards(). Instead of creating separate "open" and "close" animations, we play the same animation in reverse. Combined with await, we can hide the panel only after the animation finishes — no timers, no callbacks, just clean async GDScript.

For simpler one-off UI effects (button bounces, notification pops), tweens are usually faster to set up. See our Godot 4 Tween Tutorial for when to use tweens vs. AnimationPlayer.

Get all our Godot courses in one pack — signals, shaders, state machines, and more. Save 50%+ with the bundle.

Grab the Bundle →

Advanced Animation Tips

Once you're comfortable with the basics, here are some pro techniques to take your animations to the next level:

Animation Libraries

Godot 4 introduced Animation Libraries, which let you organize animations into named groups. Instead of dumping 50 animations into one AnimationPlayer, you can separate them by context: "combat/slash_1", "movement/run", "ui/flash_damage". Access them with anim_player.play("combat/slash_1").

Root Motion

For 3D characters (or 2D characters with precise movement), enable root motion on your AnimationTree. This lets the animation itself drive the character's position — so a dodge roll animation actually moves the character body, eliminating the disconnect between visual and physics movement.

Call Method Tracks for SFX and Particles

Method Call tracks in AnimationPlayer are incredibly powerful. Drop them onto any frame to call a function on the animated node. Use this for:

  • Playing footstep sounds at the exact frame the foot hits the ground
  • Spawning dust particles during a landing animation
  • Enabling/disabling hitbox CollisionShape2D during attack frames
  • Emitting signals for screen shake or camera effects

Blending Layers with AnimationTree

AnimationTree supports AnimationNodeBlendTree mode (different from the state machine) where you can additively blend animations. Want a character to run and aim their weapon at the same time? Use an Add2 or Blend2 node to layer the upper-body aim animation on top of the lower-body run animation. This is common in 3D games but works for 2D too if your rig is set up with separate bone tracks.

Animation Cheat Sheet — Quick Reference

Here's a quick reference for the most common animation operations in GDScript. Bookmark this — you'll use it constantly:

animation_cheatsheet.gd — Common Animation Operations
# ─── AnimationPlayer ───────────────────────────
var ap: AnimationPlayer = $AnimationPlayer

ap.play("walk")                    # Play animation
ap.play("walk", -1, 2.0)          # Play at 2x speed
ap.play_backwards("walk")         # Play in reverse
ap.queue("idle")                   # Queue after current
ap.stop()                          # Stop immediately
ap.seek(0.5, true)                 # Jump to 0.5s, update
ap.speed_scale = 0.5               # Half speed globally

# Wait for animation to finish
await ap.animation_finished

# Check current animation
if ap.current_animation == "attack":
    pass

# ─── AnimationTree State Machine ──────────────
var tree: AnimationTree = $AnimationTree
var sm: AnimationNodeStateMachinePlayback = \
    tree["parameters/playback"]

sm.travel("run")                   # Blend to "run" state
sm.start("hurt")                   # Instant switch (no blend)
sm.get_current_node()              # Current state name

# ─── BlendSpace Parameters ────────────────────
tree["parameters/walk/blend_position"] = Vector2(1, 0)
tree["parameters/speed/blend_position"] = 0.75

Pro tip: name your animations consistently. Use idle, walk, run, attack_1, attack_2 — lowercase, underscore-separated, no spaces. This makes string-based lookups reliable and keeps your project organized as it scales. For more GDScript tips, check out our 5 GDScript Tricks Most Devs Don't Know post.

AnimationPlayer vs. AnimationTree vs. Tweens — When to Use What

Godot gives you three animation systems and they're designed to work together, not compete. Here's when to reach for each:

  • AnimationPlayer Sprite sheet cycles, multi-property keyframed sequences, anything you want to edit visually in the timeline
  • AnimationTree Complex characters with 4+ states, directional movement blending, layered animations (aim + run), root motion
  • Tweens Quick one-off code animations: button bounces, damage flash, camera shake, pickup effects. Zero setup, created inline

In practice, a well-animated character often uses all three: AnimationPlayer holds the raw animations, AnimationTree manages blending and state transitions, and tweens handle reactive effects like damage flashes and hit freezes.

Wrapping Up

Godot 4's animation system is one of the engine's biggest strengths. AnimationPlayer gives you a timeline editor that can animate literally anything. AnimationTree adds state machines and blend spaces for complex character rigs. And GDScript hooks let you drive it all from code with full control.

Start simple: get AnimationPlayer working with a walk and idle cycle. Then graduate to AnimationTree when your character has more than a few states. Add BlendSpace2D when you need directional blending. And layer tweens on top for that final layer of juice.

Here's what to explore next:

Level Up
Free download
+ weekly tips

Get the Free GDScript Cheat Sheet + Weekly Godot Tips

Join indie devs getting our GDScript Cheat Sheet with 20+ copy-paste snippets for signals, exports, state machines, and more — plus weekly Godot tips and architecture tricks straight to your inbox.

20+ GDScript snippets
Weekly Godot tips
Zero spam, cancel anytime
>
GDSJoin 500+ Godot devs. Unsubscribe in one click.