How to create sprite sheets & animations for libGDX

In this tutorial you will learn how to use sprite sheets with libGDX.
Why using sprite sheets?
- By loading all graphics at once instead of numerous single images, the loading time of your app can be significantly reduced.
- Using a sprite atlas also enhances the game's performance, because textures only need to be set on the graphics device once, resulting in improved frame rates.
Creating a sprite sheet
The easiest way to create a sprite sheet is using TexturePacker — available for Windows, macOS and Linux:
Creating a sprite sheet with TexturePacker is quite simple:
- Drag and drop all the files you want to add to the left pane of TexturePacker
- Select the LibGDX framework and set the file path for the generated Data file. The Texture file can be left empty; TexturePacker will automatically save the PNG containing the sprite sheet next to the data file.
- Click on Publish sprite sheet
We recommend storing the individual sprites in a root folder named raw-assets, while only placing the sprite sheet data and texture file in the assets folder. This makes it easier to determine which assets must be deployed later.
Using a sprite sheet
Let's start with a new, empty project as you can generate it with the libGDX Project Setup Tool. The default implementation of the main class in the core module loads a sprite called libgdx.png and displays it on a dark background.
In the previous section, we've already added the libgdx.png sprite to the sprite sheet. Now, we can load the sprite sheet and extract the sprite from it:
package com.mygdx.game;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.ScreenUtils;
/** {@link com.badlogic.gdx.ApplicationListener} implementation shared by all platforms. */
public class MyGdxGame extends ApplicationAdapter {
private SpriteBatch batch;
private TextureAtlas textureAtlas;
private Sprite sprite;
@Override
public void create() {
batch = new SpriteBatch();
textureAtlas = new TextureAtlas("spritesheet.atlas");
sprite = textureAtlas.createSprite("libgdx");
}
@Override
public void render() {
ScreenUtils.clear(0.57f, 0.77f, 0.85f, 1);
batch.begin();
sprite.setPosition(10, 10);
sprite.draw(batch);
batch.end();
}
@Override
public void dispose() {
batch.dispose();
textureAtlas.dispose();
}
}We've also changed the background to a bright blue color:
Creating an animation
Now, we want to replace the static sprite with an animation. TexturePacker can automatically detect animation sprites: If the sprite filenames only differ by a sequence number suffix, TexturePacker will remove this suffix and export all the sprites as animation frames in the data file.
This makes it very easy to create an animation from a sprite sheet:
package com.mygdx.game;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.ScreenUtils;
/** {@link com.badlogic.gdx.ApplicationListener} implementation shared by all platforms. */
public class MyGdxGame extends ApplicationAdapter {
private SpriteBatch batch;
private TextureAtlas textureAtlas;
private Animation<Sprite> animation;
private float stateTime = 0;
@Override
public void create() {
batch = new SpriteBatch();
textureAtlas = new TextureAtlas("spritesheet.atlas");
animation = new Animation<>(0.066f, textureAtlas.createSprites("capguy"), Animation.PlayMode.LOOP);
}
@Override
public void render() {
ScreenUtils.clear(0.57f, 0.77f, 0.85f, 1);
stateTime += Gdx.graphics.getDeltaTime();
Sprite sprite = animation.getKeyFrame(stateTime, true);
sprite.setX(stateTime * 250 % (Gdx.graphics.getWidth() + 400) - 200);
batch.begin();
sprite.draw(batch);
batch.end();
}
@Override
public void dispose() {
batch.dispose();
textureAtlas.dispose();
}
}The Animation object acts as a container for animation frames. You can retrieve the frame to display at
a specific point in time using the getKeyFrame method. Note that you need to track the elapsed time externally.
The complete example project is available on GitHub
Using texture compression
Texture compression reduces the amount of GPU memory a texture occupies and can speed up rendering. Unlike PNG or JPEG, which are decompressed into raw pixels before they reach the GPU, compressed textures stay compressed in video memory. This means lower memory usage and faster texture uploads.
libGDX supports KTX files with ASTC, ETC1/ETC2 and DXT (S3TC) compression, which can be generated with TexturePacker. With an additional library it also loads KTX2 files with Basis Universal compression, see Using KTX2 and Basis Universal below. For a full overview of all formats supported by TexturePacker, see Texture compression.
| Format | bpp | Platform |
|---|---|---|
| ASTC | 0.89 - 8 | iOS, modern Android |
| ETC1 | 4 | All OpenGL ES devices (no alpha) |
| ETC2 | 4 - 8 | OpenGL ES 3.0+ |
| DXT1 | 4 | Desktop (1-bit transparency) |
| DXT5 | 8 | Desktop (full alpha) |
| Basis Universal (ETC1S) | 0.3 - 1.25 | All platforms (KTX2 only, needs gdx-basis-universal) |
| Basis Universal (UASTC) | 4 - 8 | All platforms (KTX2 only, needs gdx-basis-universal) |
Exporting ASTC, ETC2, DXT and Basis Universal textures for libGDX requires TexturePacker 7.10.0 or newer.
Using KTX2 and Basis Universal
The formats above are tied to specific GPUs: DXT is a desktop format, ETC and ASTC are made for mobile devices. A game that runs on both needs a separate sprite sheet for each. Basis Universal solves this with one texture file for all platforms. It is transcoded at load time into a compressed format the device supports.
Basis Universal offers two encoding modes:
| Mode | bpp (file) | Use case |
|---|---|---|
| ETC1S | 0.3 - 1.25 | Smallest download size, medium quality |
| UASTC | 4 - 8 | High quality, larger files |
To create a Basis Universal sprite sheet in TexturePacker, select these texture settings:
- Texture format: KTX2
- Pixel format: BASISU_UASTC or BASISU_ETC1S
- Size constraints: Compression blocksize (
CompressionBlocksizeon the command line)
The size constraint matters. The transcoder can only pick a compressed GPU format if the texture size fits that format, and most formats need a width and height divisible by four. If the size does not fit, the texture is silently decoded to uncompressed RGBA and you lose the memory savings.
If you target iOS devices that only support PVRTC, use POT as size constraint and enable Force squared instead: PVRTC needs a square texture with power-of-two sides.
On the command line, the same sprite sheet is created with:
TexturePacker --format libgdx --texture-format ktx2 --pixel-format BASISU_UASTC \
--size-constraints CompressionBlocksize \
--data assets/spritesheet.atlas --sheet assets/spritesheet.ktx2 raw-assetsLoading KTX2 files in libGDX
libGDX cannot read .ktx2 files out of the box. new TextureAtlas("spritesheet.atlas") fails with
Couldn't load file: spritesheet.ktx2. The open source library
gdx-basis-universal adds the missing loader.
Add it to the dependencies of your core module:
dependencies {
api "com.crashinvaders.basisu:basisu-wrapper:1.1.2"
api "com.crashinvaders.basisu:basisu-gdx:1.1.2"
}Version 1.1.2 of the library is built for libGDX 1.14.2. If your project uses an older libGDX version,
update gdxVersion in gradle.properties.
Each platform also needs the native transcoder. For the desktop (LWJGL3) module this is:
dependencies {
runtimeOnly "com.crashinvaders.basisu:basisu-wrapper:1.1.2:natives-desktop"
}The dependencies for Android, iOS and the web backend are listed in the gdx-basis-universal documentation.
Now register the Ktx2TextureLoader for the .ktx2 file extension and load the sprite sheet through
an AssetManager. The atlas file references the .ktx2 texture, the asset manager picks the right loader for it:
package com.mygdx.game;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.utils.ScreenUtils;
import com.crashinvaders.basisu.gdx.Ktx2TextureLoader;
public class MyGdxGame extends ApplicationAdapter {
private SpriteBatch batch;
private AssetManager assetManager;
private Sprite sprite;
@Override
public void create() {
batch = new SpriteBatch();
assetManager = new AssetManager();
assetManager.setLoader(Texture.class, ".ktx2",
new Ktx2TextureLoader(assetManager.getFileHandleResolver()));
assetManager.load("spritesheet.atlas", TextureAtlas.class);
assetManager.finishLoading();
TextureAtlas textureAtlas = assetManager.get("spritesheet.atlas", TextureAtlas.class);
sprite = textureAtlas.createSprite("libgdx");
}
@Override
public void render() {
ScreenUtils.clear(0.57f, 0.77f, 0.85f, 1);
batch.begin();
sprite.setPosition(10, 10);
sprite.draw(batch);
batch.end();
}
@Override
public void dispose() {
batch.dispose();
assetManager.dispose();
}
}The asset manager owns the atlas and its texture, so disposing the asset manager is enough. Sprites and animations work exactly as in the sections above.
To check which GPU format was chosen, enable debug logging with
Gdx.app.setLogLevel(Application.LOG_DEBUG). The library then prints a line such as
Transcoding to the BC3_RGBA format for each texture. If it says RGBA32, the texture is loaded uncompressed:
check the size settings above.