How to create Sprite Sheets and Animations for Kivy

Andreas Löw
Last updated:
GitHub
How to create Sprite Sheets and Animations for Kivy

What you are going to learn

In this tutorial, you'll learn how to:

  • Create optimized sprite sheets for your Kivy app or game
  • Implement a sprite animation
  • Optimize memory usage and rendering performance

Prerequisites

Before starting this tutorial, you should have:

  • Python 3.8+ installed
  • Basic familiarity with Python

Step 1: Create a new Kivy project

Let's start by creating a new folder for the project and initialise a new python environment inside:

mkdir kivy-sprite-sheet-example
cd kivy-sprite-sheet-example

python -m venv venv

On windows activate with

call venv\Scripts\activate.bat

on linux / macos

source venv/bin/activate

install kivy

pip install kivy

Step 2: Download the assets

Download the assets for this project from GitHub and extract them into the projects' folder.

The files consist of a walking animation for a character and a background image.

In addition to that, it also contains a cityscene.tps which is a TexturePacker project file we'll use to create the sprite sheet.

assets-raw
├── cityscene
│   ├── capguy
│   │    └── walk
│   │        ├── 0001.png
│   │        ├── 0002.png
│   │        ├── 0003.png
│   │        ├── ...
│   └── background.png
├── cityscene.tps

This folder is called assets-raw because we consider this the input assets that are not yet processed or optimized for the demo.

Note: We use Zero-padded numbers - e.g. 0001 instead of 1 which helps us to easily build animations in Kivy later.

While here, also create a new folder to store the optimized assets in:

mkdir assets-optimized

Step 3: Prepare the Graphics for the Demo

Using individual sprites versus sprite sheets has significant performance implications:

AspectIndividual SpritesSprite Sheets
Loading👎 Slow because each file is loaded separately👍 Single file loads much faster.
File Size👎 Larger sizes (unoptimized)👍 20-40% smaller with optimization (e.g. png compression)
Rendering👎 Many draw calls = lower FPS👍 Batched rendering = higher FPS
Management👎 Complex asset organization👍 One file replaces dozens

The solution is to pack your graphics into a sprite sheet (texture atlas) using a tool like TexturePacker.

Why TexturePacker?

TexturePacker creates optimized sprite sheets with automatic packing algorithms, rotation, and trimming. Key benefits include:

  • Perpetual licensing: Pay once, use forever—no subscriptions. Generous discounts for indies.
  • Regular updates with new features and bug fixes
  • Free support included with every license
  • Platform flexibility: Works with virtually every game engine, giving you complete freedom
  • Long-term commitment: We're not a hobby project that gets abandoned — TexturePacker has been actively developed since 2010, and we're here to stay

You can follow along with the free trial version included in this tutorial.

Packing the Sprite Sheet

Step 1: Launch TexturePacker Open the TexturePacker application.

Step 2: Add Your Sprites Drag and drop the assets-raw/cityscene folder onto the TexturePacker window. TexturePacker automatically detects and adds all sprites in that folder to the sheet.

Step 3: Configure Settings

  • Texture Format: Select PNG-8 to reduce file size by up to 75%.
  • Framework: Click the field next to Framework and choose Kivy from the list.
  • Atlas File: Click the folder icon next to the Atlas File field, navigate to your assets-optimized folder, and name the file cityscene.atlas.
  • Other Settings: Leave all other settings at their default values.

Step 4: Save Your Project Save the configuration as a .tps project file (e.g., in the assets-raw folder). This makes it easy to update the sprite sheet later — just add new sprites to the cityscene folder and re-publish.

Step 5: Generate the Sprite Sheet Click Publish to generate the optimized sprite sheet and atlas file.

Using TexturePacker with Kivy
Using TexturePacker with Kivy

TexturePacker offers many additional options to fine-tune optimization — but for now, the default settings are a great starting point. One feature you may find useful soon is Multipack: when enabled, TexturePacker automatically splits your sprites across multiple sheets if a single one isn’t large enough. This allows you to keep adding assets without worrying about hitting texture size limits.

Additionally, TexturePacker includes a powerful command-line interface that allows you to update sprite sheets automatically — perfect for integration with build scripts.

Step 4: Displaying a static background

Now let’s get into the fun part — coding! 🎉 That’s what you’re here for, right?

Create a src folder to hold your game source code and create a main.py in it.

import kivy
kivy.require('2.3.0')

from kivy.atlas import Atlas
from kivy.app import App
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import Image

class CitySceneApp(App):
    def build(self) -> FloatLayout:
        layout = FloatLayout()

        # Add background image
        background = Image(
            source='atlas://assets-optimized/cityscene/background',
            fit_mode="fill"
        )
        layout.add_widget(background)

        return layout

if __name__ == '__main__':
    CitySceneApp().run()

The CitySceneApp class extends Kivy’s App and overrides the build() method to construct the user interface. It uses a FloatLayout, which allows widgets to be freely positioned using relative coordinates.

An Image widget is added to the layout to display the background. The source is set using the Kivy atlas syntax: atlas://assets-optimized/cityscene/background, which tells Kivy to load the background sprite from the cityscene.atlas in the assets-optimized folder.

The image is configured to scale to fill the window fit_mode="fill"

Finally, CitySceneApp().run() starts the app and opens the window with the rendered background. This provides a solid visual foundation for building more complex animated scenes.

Start it using

python src/main.py

And you should see

Kivy game scene with static background
Kivy game scene with static background

Step 6: Creating an animated character

Let's now create an animated character that walks from left to right. For this, we use the Image() class of Kivy combined with an Animation().

Animation helper function

This short function grabs all animation frames from an atlas and returns them in order:

def frames_for_animation(atlas_path, prefix):
    """Return a list of frame sources from a Kivy atlas for animation."""
    atlas = Atlas(f"{atlas_path}.atlas")
    keys = sorted(k for k in atlas.textures if k.startswith(prefix))
    return [f"atlas://{atlas_path}/{k}" for k in keys]

You can use this function this way

frames_for_animation("assets-optimized/cityscene", "capguy_walk_")

The first parameter is the atlas to use, the second the prefix of the animation. Note that texturepacker converts the frame names by concatenating folders with _ and omitting the file name extension. E.g. capguy/walk/0001.png becomes capguy_walk_0001.

The animated character class

AnimatedCharacter is the sprite that walks across the scene. Under the hood it's just a kivy.uix.image.Image, but we give it a few extras. It loads the sprite frames from the function we created before and uses a timer to update the animation and move.

# Animation constants
FRAME_RATE = 6.0           # frames per second
PIXELS_PER_FRAME = 24.0    # pixels per second

class AnimatedCharacter(Image):
    """Image widget that cycles through frames to animate a character."""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.frames = frames_for_animation(
            "assets-optimized/cityscene", "capguy_walk_"
        )
        # start position, initial frame
        self.x = -self.width
        self.y = 240
        self.frame_index = 0
        self.size_hint = (None, None)
        self.source = self.frames[self.frame_index]
        self.bind(texture=self._on_texture)
        # schedule updates
        Clock.schedule_interval(self._update, 1.0 / FRAME_RATE)

    def _update(self, dt: float) -> None:
        """Advance frame and position each tick."""
        self.frame_index = (self.frame_index + 1) % len(self.frames)
        self.source = self.frames[self.frame_index]
        self.x += PIXELS_PER_FRAME
        if self.x >= Window.width + 2 * self.width:
            self.x = -self.width

    def _on_texture(self, instance, texture) -> None:
        """Set image size when texture loads."""
        self.size = texture.size

A quick thought about the type of animation we are using:

A walking animation

When the animation frame changes, we also adjust the sprite’s position. If we did not synchronize both actions, the legs would not move between the frames, but the character would appear to slide — or “moon‑walk” — across the floor between frames.

What about other kinds of objects?

For assets whose movement is independent of their animation — think of a starship that glides smoothly while its thrusters flicker — you would set FRAME_RATE to 60 fps to make the movement smooth and update the frames at a lower rate.

Adding the animation to the Scene:

The final step is to add the AnimatedCharacter() to the scene:

class CitySceneApp(App):
    """Kivy application to display the animated city scene."""

    def build(self):
        layout = FloatLayout()
        background = Image(
            source="atlas://assets-optimized/cityscene/background",
            fit_mode="fill",
        )
        layout.add_widget(background)
        # highlight-start
        layout.add_widget(AnimatedCharacter())
        # highlight-end
        return layout

Step 6: Performance & Memory Optimization

To further improve your app’s performance and reduce memory usage, consider the following techniques. We won’t go into detail here to keep the tutorial concise, but these tips should give you a good starting point for further optimization.

Adapting to screen resolutions

Optimize your atlas dimensions according to the capabilities of your target devices.

In the Advanced Settings in TexturePacker, you’ll find an option called Scaling Variants. This feature allows you to generate multiple versions of your sprite sheet at different resolutions.

At runtime, you can select the most suitable variant based on the screen or window size — using high-resolution assets for large displays, and smaller, optimized textures for lower-resolution screens.

This approach improves performance by avoiding the overhead of rendering large textures on small screens, and ensures sharp, crisp visuals on high-resolution displays.

Colors and image formats

The PNG-8 format we used in our TexturePacker settings reduces the total number of colors in the sprite sheet to 256 and applies dithering. In most cases, this delivers good visual quality while dramatically reducing the file size of the atlas.

However, it does not reduce the amount of RAM required to render the sheet!

If you notice artifacts on your sprites or require more colors, consider switching to PNG-32.

Another option is to group sprites with the same color on a single sheet - e.g. everything that is blue on one, red on another.

Multipack

If your game uses many sprites, the default texture size of 2048×2048 may not be sufficient. While you can increase the texture size, some hardware may impose limitations.

TexturePacker can automatically split your sprites across multiple sprite sheets — this feature is called multipack. You still get a single atlas file, but it references multiple atlas images. Your application will not notice any difference.

You can also choose to deliberately split sprites across sheets. For example, if a character appears in multiple scenes, you can place it on a separate sheet to enable reuse.