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:
Aspect | Individual Sprites | Sprite 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 filecityscene.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.

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
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.