← All articles

Godot 4 Sprite Animation: AnimatedSprite2D or AnimationPlayer?

October 8, 2026 · 8 min read · Updated October 14, 2026

When to animate a sprite sheet with AnimatedSprite2D or AnimationPlayer in Godot 4, how to set up each, and the GDScript to play, switch and flip.

Open the tool from this article — free, in your browser, no signup.

You have a sprite sheet, and Godot 4 offers two ways to animate it: AnimatedSprite2D and AnimationPlayer. Both work, but they solve different problems, and picking the right one up front saves you rebuilding animations later.

This article explains the difference, sets both up from the same sheet, and gives you the GDScript that drives them. It applies to all of Godot 4; the one feature that is new in Godot 4.7 is marked.

Which one to use

Left: AnimatedSprite2D with idle, hop and attack frame lists, each with its own FPS. Right: an AnimationPlayer timeline with keys for the sprite's frame, two method calls that switch a hitbox on and off, and a sound
The slime from this article both ways. AnimatedSprite2D plays lists of frames; AnimationPlayer puts the frame on a timeline next to everything that must happen with it.

AnimatedSprite2D plays a SpriteFrames resource: named animations, each a list of frames with its own speed. AnimationPlayer is a general timeline. Point it at a Sprite2D and it can key the sprite's frame number, plus any other property, method call or sound at an exact time.

AnimatedSprite2D Sprite2D + AnimationPlayer
You edit Frame lists and an FPS per animation Keys on a timeline, in seconds
Holding a frame longer A relative duration per frame Move the next key later
Sheets with a border or gaps Supported when you import frames Not supported: Hframes × Vframes must be an even grid
Changing other things on a frame In code, through signals On the same timeline, no code
Looping On or off (ping-pong added in Godot 4.7) None, loop or ping-pong

A good rule: start with AnimatedSprite2D. Move an animation to AnimationPlayer when something besides the picture has to change on a particular frame: a hitbox during a swing, a footstep sound, a muzzle flash. The two also combine, since an AnimationPlayer can key an AnimatedSprite2D's properties too.

Set up AnimatedSprite2D from a sheet

The example is a 192 × 96 slime sheet (free to download at the end): three rows of 32 × 32 frames for idle (4 frames), hop (6) and attack (5).

  1. Add an AnimatedSprite2D node. In the Inspector, click its empty Sprite Frames property, choose New SpriteFrames, then click the new resource. The SpriteFrames panel opens at the bottom of the editor.
  2. Double-click the default animation and rename it idle.
  3. Click Add Frames from Sprite Sheet (the grid icon above the frame list; Ctrl+Shift+O, or Cmd+Shift+O on a Mac) and pick the PNG.
  4. In the Select Frames dialog, set Horizontal to 6 and Vertical to 3. Click the four idle frames, then Add 4 Frame(s).
  5. Set the speed in the FPS box beside the animation list; new animations start at 5 FPS. The Animation Looping button next to it turns looping on or off. In Godot 4.7 it has a third state, ping-pong.
  6. Click Add Animation and repeat for hop and attack.

Two settings in the panel do most of the timing work. FPS sets the base speed, and Frame Duration stretches individual frames. Frame Duration is relative, and the SpriteFrames documentation gives the formula: seconds on screen = duration ÷ (FPS × playing speed). At 10 FPS, a frame with duration 1.0 shows for 0.1 s and one with 3.0 holds for 0.3 s, which is how you give an attack a pause before the strike. For choosing the numbers, see How Fast Should Pixel Art Animate?

Sheets with a border or gaps

If the sheet has a border around it (a margin) or gaps between frames (spacing), the dialog's other fields matter: Size is the frame size, Separation is the gap and Offset is the border. The order you fill them in decides the result.

A coin sheet with a 4-pixel border and 2-pixel gaps, labeled with Godot's Offset, Separation and Size fields. Typing Size first gives 4 by 2 frames of 32 by 32; typing Horizontal and Vertical gives 33 by 34 frames
When the sheet has a border, type the Size. If you type the counts instead, the right and bottom border end up inside the frames.

When you type Horizontal and Vertical, Godot works out the size by dividing what's left of the image after the Offset and the gaps. The Offset only comes off the left and top, so the border on the right and bottom is counted as frame. On this 142 × 74 coin sheet that gives 33 × 34 frames, and each cut lands a little further off target. Type Size first instead (32 × 32), then Separation (2) and Offset (4), and Godot works out the counts itself. If you don't know the numbers, How to Find the Frame Size of a Sprite Sheet shows how to work them out.

Play, switch and flip from GDScript

This character idles, hops while moving and attacks once on a key press:

extends CharacterBody2D

const SPEED := 120.0

@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D

var attacking := false


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


func _physics_process(_delta: float) -> void:
	var direction := Input.get_axis("ui_left", "ui_right")
	velocity.x = direction * SPEED
	move_and_slide()

	if direction != 0.0:
		sprite.flip_h = direction < 0.0

	if attacking:
		return
	if Input.is_action_just_pressed("ui_accept"):
		attacking = true
		sprite.play("attack")
	elif direction != 0.0:
		sprite.play("hop")
	else:
		sprite.play("idle")


func _on_animation_finished() -> void:
	if sprite.animation == &"attack":
		attacking = false

Four details make this work:

  • Calling play() every frame is safe. If the animation is already playing, play() with the same name carries on instead of restarting, so you don't need to check first.
  • animation_finished never fires for a looping animation, as the AnimatedSprite2D reference notes. Turn looping off for attack, or the character stays stuck in it.
  • flip_h mirrors the drawing only. Child nodes such as a hitbox keep their position, so mirror their position.x in the same place you set flip_h.
  • Per-frame events need a signal. For a footstep, connect frame_changed and check sprite.animation and sprite.frame there. If you find yourself writing many of these checks, that animation belongs in an AnimationPlayer.

The AnimationPlayer route

Here the picture comes from a plain Sprite2D, and the AnimationPlayer changes its frame number on a timeline.

  1. Add a Sprite2D and drag the sheet onto its Texture. Under Animation in the Inspector, set Hframes to 6 and Vframes to 3. Frames are numbered left to right, top to bottom from 0, so the attack row is frames 12 to 16. Sprite2D cuts the texture into equal cells, so it can't skip a border or gaps; give it a sheet without them.
  2. Add an AnimationPlayer, open the Animation panel and choose Animation → New. Name it attack and set its length to 0.5 s: five frames at 10 FPS.
  3. At the bottom of the panel, switch the snap step from Seconds to FPS and enter 10, so keys land every 0.1 s.
  4. Select the Sprite2D and set Frame to 12. Click the key icon next to Frame and confirm the new track. Godot adds the key, raises Frame to 13 and moves the playhead one step. Click it four more times to key 13 to 16. The track is created in Discrete mode, so it jumps between whole frames instead of blending.
  5. To sync gameplay, add a Call Method Track for the character node (Add Track → Call Method Track…). Right-click it at 0.1 s, choose Insert Key…, and pick enable_hitbox; insert disable_hitbox at 0.3 s. An Audio Playback Track can start the swipe sound at 0.1 s too.

The methods the track calls live in the character's script. Here the hitbox is an Area2D child named Hitbox:

func enable_hitbox() -> void:
	$Hitbox/CollisionShape2D.set_deferred("disabled", false)


func disable_hitbox() -> void:
	$Hitbox/CollisionShape2D.set_deferred("disabled", true)

set_deferred() is there because Godot's documentation says a collision shape's disabled property should be changed that way. Start the animation with $AnimationPlayer.play("attack") and listen for its animation_finished(anim_name) signal; like the AnimatedSprite2D signal, it never fires while the animation loops. One more trap from the 2D sprite animation tutorial: play() takes effect the next time the player processes. If you flip the sprite in the same frame and see one glitched frame, call advance(0) after play().

Exporting a ready-made SpriteFrames from SpritePilot

If you cut your sheet with SpritePilot's Grid Slice, or bring frames in from a GIF or the Pixel Art Editor, Export & Download can write the SpriteFrames resource for you. Choose SpriteFrames (Godot 4 .tres resource) and add an Animation Tag for each animation.

SpritePilot's Export settings with the SpriteFrames format selected and three animation tags: idle frames 0 to 3, hop 4 to 9, attack 10 to 14
Each tag becomes a named animation in the .tres file.

The download is the packed sheet, sprite-sheet.png, and sprite-frames.tres, a text resource that starts like this:

[gd_resource type="SpriteFrames" load_steps=17 format=3]

[ext_resource type="Texture2D" path="res://sprite-sheet.png" id="1"]

[sub_resource type="AtlasTexture" id="AtlasTexture_1"]
atlas = ExtResource("1")
region = Rect2(0, 0, 32, 32)

What's inside:

  • One AtlasTexture per frame, pointing at that frame's rectangle in the sheet. With Trim transparency on, each also gets a margin that restores its original size, so trimmed frames don't shift.
  • One animation per tag, using the frames from–to counted from 0 in the export order. With no tags you get a single animation called default containing every frame.
  • 12 FPS, looping, duration 1.0 for every animation. Adjust them in the SpriteFrames panel afterwards, and switch looping off for attack. A tag's reverse or ping-pong direction is not carried into the file.

The resource expects the sheet at res://sprite-sheet.png, so put both files in the project root, or edit the path on the ext_resource line. Then load sprite-frames.tres into the AnimatedSprite2D's Sprite Frames property. The Godot import guide covers the JSON alternative.

A checklist

  • Only the picture changes: AnimatedSprite2D.
  • Something must happen on a specific frame: AnimationPlayer, or frame_changed in code for one or two cases.
  • Sheet with a border: type the Size in the Select Frames dialog, not the counts.
  • Anything you wait for, such as an attack or a death, must not loop.
  • flip_h mirrors the drawing, not its children.
  • For crisp pixel art, set the project's default texture filter to Nearest (why lines appear between tiles explains it).

The slime sheet is free to use in any project (CC0): download slime-sheet.png, 192 × 96, with idle in the top row (4 frames), hop in the middle row (6) and attack in the bottom row (5), all 32 × 32. Check its timing in the Sprite Player before you build the animations. The same sheet is set up in Unity, Phaser and pygame too. For more to practice with, the Free Pixel Art Game Assets page has 70 animated CC0 sprites, from creatures to effects, as one-row strips that load the same way: Horizontal = the frame count, Vertical = 1.

More articles