gif→sprite sheet

Godot

Godot Sprite Sheet Animation: AnimatedSprite2D and Hframes

Animate a sprite sheet in Godot 4 with AnimatedSprite2D or Sprite2D Hframes and Vframes, keep pixel art sharp, and get a GIF in when Godot won't import it.

Updated 9 min read

Put the PNG in your project, add an AnimatedSprite2D, give it a new SpriteFrames resource, and use Add frames from a Sprite Sheet with the sheet’s column and row counts. That’s the whole job for a flipbook animation in Godot 4, and it takes about a minute once you know the two numbers. The rest of this page is the other route (Sprite2D with Hframes and Vframes), what to do when your animation is a GIF, and the five ways this usually goes wrong.

Everything here was checked against the Godot 4.7 manual and class reference. The .tres and script parts were also run in Godot 4.7 stable. I say below exactly what was run and what wasn’t.

Which node to use

AnimatedSprite2D + SpriteFramesSprite2D + AnimationPlayer
How the sheet is cutYou pick cells in a dialog. Spacing and offset are supported.Hframes × Vframes divide the texture into equal cells. No spacing.
Several animations on one sheetOne named animation per set of cellsOne AnimationPlayer animation per set of Frame keys
TimingFPS per animation, plus a duration multiplier per frameYou place each key on the timeline
Animating other things in stepNoYes: position, collision shapes, sounds, method calls

My rule: if the animation is only pictures changing, use AnimatedSprite2D. People reach for AnimationPlayer because it looks like the serious tool, then spend ten minutes keying a frame number six times. Switch to it on the day an attack needs its hitbox to appear on frame 3.

AnimatedSprite2D: frames from a sheet

  1. Drag the PNG into the FileSystem dock. Godot imports it on its own.
  2. Add an AnimatedSprite2D node. In the Inspector, click the empty Sprite Frames property and choose New SpriteFrames, then click the new resource. The SpriteFrames panel opens at the bottom of the editor.
  3. In that panel click Add frames from a Sprite Sheet and open your PNG.
  4. Set Horizontal to the number of columns and Vertical to the number of rows. A sheet that is 4 frames wide and 2 tall is Horizontal 4, Vertical 2.
  5. Click the frames you want in this animation, then press the add button (it reads Add 4 Frame(s) with your count).
  6. Double-click default in the animation list to rename it, type the speed into the FPS box (the manual calls it Speed (FPS)), and leave Animation Looping on for a cycle or turn it off for a one-shot.
  7. Press play in the panel to preview.

Two things about that dialog that tutorials skip. Horizontal and Vertical each stop at 128 in the 4.7 editor, so a 200-frame single-row strip can’t be cut there; re-export it as a grid. And the dialog has Size, Separation and Offset fields for sheets with gaps between cells or a border around the outside. If your sheet has 2 px between frames, that goes in Separation.

For a character sheet with a row per action, repeat steps 3 to 6 for each row: add an animation, open the same PNG, select that row’s cells.

Make it play in the game

The preview in the panel is only a preview. In the running game an AnimatedSprite2D sits on frame 0 until something starts it. Either select the animation and turn on Autoplay on Load in the SpriteFrames panel (in 4.7 there is no autoplay field in the Inspector, so don’t look for it there), or call play():

extends Node2D

@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D

func _ready() -> void:
	sprite.sprite_frames = load("res://blob-sheet.tres")
	sprite.play("default")

That script ran as written in Godot 4.7. play() takes the animation name, and stop() resets to frame 0. If you connect animation_finished to chain into another animation, know that the signal is never emitted for a looping animation.

Frames that should last longer

Each frame in SpriteFrames has a Frame Duration, a multiplier that defaults to 1. A frame set to 2 stays up twice as long. The class reference gives the formula: seconds on screen = duration ÷ (animation FPS × playing speed). So at 10 FPS, a frame with duration 4 is held for 0.4 seconds. Select a frame in the panel and change the number; you don’t need to duplicate the frame four times.

Sprite2D with Hframes and Vframes

  1. Add a Sprite2D and an AnimationPlayer to the scene. Drag the sheet into the sprite’s Texture property. You’ll see the whole sheet.
  2. In the Inspector open Animation and set Hframes to the column count and Vframes to the row count. Now one cell shows.
  3. Change Frame to check the cut. Frames count from 0, left to right, then down a row. On a 4 × 2 sheet the last frame is 7.
  4. Select the AnimationPlayer, click Animation → New, name it, set the length (0.6 seconds for 6 frames at 10 fps) and turn on looping.
  5. Select the Sprite2D again. With Frame at 0, click the key icon next to Frame to create the track. Then add a key for each following frame, one snap step apart (0.1 seconds by default), until the last frame is keyed.

Hframes and Vframes do one thing: divide the texture’s width by Hframes and its height by Vframes. There’s no spacing setting, which is why a sheet with gaps works in the SpriteFrames dialog and comes out wrong here. As with the other route, nothing moves until the AnimationPlayer has an autoplay animation or you call play("walk") on it.

Keeping pixel art sharp

Godot 4’s default canvas filter is Linear, which smooths between pixels as soon as a sprite is scaled or sits on a half pixel. For a pixel art project, change it once: Project Settings → Rendering → Textures → Canvas Textures → Default Texture Filter → Nearest. The choices in 4.7 are Nearest, Linear, Linear Mipmap and Nearest Mipmap.

If you want one sprite sharp and the rest smooth, set it per node instead: CanvasItem → Texture → Filter → Nearest in the Inspector. Godot 3 had a Filter option in the Import dock. Since 4.0 the filter lives on the node and in that project setting, so advice that says to untick Filter and reimport is for the old version.

Godot does not import GIFs

The 4.7 manual lists the image formats Godot imports: BMP, DDS, KTX, EXR, HDR, JPEG, PNG, TGA, WebP and SVG. GIF isn’t on it. I dropped a three-frame GIF into a 4.7 project to see what happens: Godot created no .import file for it, and loading it from a script fails with

ERROR: No loader found for resource: res://anim.gif (expected type: unknown)

This isn’t about to change. A pull request adding GIF import was closed unmerged, and the proposal to support animated images was closed as not planned in 2021. So the way in is to turn the GIF into a PNG sheet. Our GIF to sprite sheet converter does that in the browser, and under the preview it prints the two numbers this page keeps asking for (for example Hframes 4, Vframes 2) along with the fps.

There’s a catch with GIFs that a plain sheet hides. A GIF stores a delay per frame, and animators use that for holds: a blink that lasts 100 ms after an open eye that lasts two seconds. A PNG has no timing in it, so at one fixed FPS the holds vanish. You can fix that by hand with Frame Duration, or skip the handwork with the file described next.

The .tres export and what I tested

The converter’s Godot .tres button downloads a SpriteFrames resource as text. It has one AtlasTexture per cell with the cell’s rectangle, and a single looping animation called default whose speed is the GIF’s average frame rate and whose frame durations are each frame’s delay relative to that average:

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

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

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

...

[resource]
animations = [{
"frames": [{
"duration": 0.667,
"texture": SubResource("AtlasTexture_0")
}, ...],
"loop": true,
"name": &"default",
"speed": 6.67
}]

To use it, put the PNG and the .tres in the project, then load the .tres into the Sprite Frames property of an AnimatedSprite2D, or load() it from a script as above.

The one thing you must get right is the path= line. The file expects the PNG at the project root under the name it was downloaded with, res://blob-sheet.png here. If you keep art in a folder, open the .tres in a text editor and change that line to match, such as res://art/blob-sheet.png. With the path wrong, the resource fails to load:

ERROR: Resource file not found: res://art/missing.png (expected type: Texture2D)
ERROR: res://wrongpath.tres:6 - Parse Error: [ext_resource] referenced non-existent resource at: res://art/missing.png.

What I ran: Godot 4.7 stable on macOS, started headless from the command line with test scripts. I did not click through the editor for this article, so the editor steps above come from the manual and the 4.7 editor source, not from my screen. The test file was generated by the site’s own export code for a 4 × 2 sheet of 32 px cells with delays of 100, 100, 100, 400, 100, 100, 100 and 200 ms. Godot loaded it as 8 frames at speed 6.67, each frame showed the right cell, and after the first loop the frame changes arrived 90 to 108 ms apart for the 100 ms frames, 407 ms for the held one and 186 ms for the last. The edited-path version loaded too.

Limits worth knowing: it writes one animation per file, so a GIF per action means a .tres per action, and it always sets loop on. Turn off Animation Looping in the panel for a one-shot. If you already have a sheet and want to see how it plays before opening Godot, the sprite sheet to GIF tool plays a grid back at 1 to 60 fps.

What goes wrong

The sprite is blurry

That’s the Linear default filter. Set Default Texture Filter to Nearest as above. If it’s sharp standing still and shimmers while moving, the sprite is landing on fractions of a pixel; Project Settings → Rendering → 2D → Snap → Snap 2D Transforms to Pixel rounds positions, at the cost of slightly less smooth movement.

Frames are cut in the wrong place

Hframes is columns and Vframes is rows, and they get swapped a lot. Count across first. If the counts are right and each frame still shows a sliver of its neighbour that grows along the row, the sheet has spacing. Take 4 columns of 32 px cells with 2 px between them: the image is 134 px wide, Hframes 4 makes every cell 33.5 px, and each frame drifts further off. A Sprite2D can’t correct that. Use the SpriteFrames dialog with Separation 2, or re-export the sheet with spacing 0.

Frame index out of bounds

Setting frame on a Sprite2D past the last cell prints this and leaves the frame where it was:

ERROR: Index p_frame = 8 is out of bounds (vframes * hframes = 8).

Frames start at 0, so 8 cells means 0 to 7. The other cause is order: a script sets frame before hframes and vframes, while the node still thinks it has one cell. Empty cells at the end of a grid count too. A 10-frame animation on a 4 × 3 sheet has frames 10 and 11 blank, so stop your keys at 9.

It plays in the editor but not in the game

Nothing started it. Turn on Autoplay on Load or call play() in _ready(). If you call play("run") and nothing happens, check that the name matches the animation list exactly; a fresh SpriteFrames calls its animation default.

The sheet has a border or uneven cells

A margin around the outside goes in the dialog’s Offset. For a Sprite2D, turn on Region and set the rect to the area inside the margin, since Hframes and Vframes then divide that region. Sheets where every sprite is a different size (a lot of AI-generated and ripped sheets) won’t cut on any grid. Run those through the sprite sheet cutter, which finds each sprite and pads them to one size aligned by the feet, then pack the frames into an even grid with the sprite sheet maker before bringing them to Godot.