⚠️ Beta State
PyBevy is in an early and experimental stage. The API is incomplete, subject to breaking changes without notice, and you should expect bugs. Many features are still under development.
Sprite Animation
Create a looping animation from a sprite sheet.
Introduction
Animating 2D characters is often done using a "sprite sheet" or "texture atlas", which is a single image containing all the frames of an animation.
pybevy provides TextureAtlasLayout and TextureAtlas components to make this easy.
from pybevy.prelude import *
from pybevy.image import TextureAtlasLayout, TextureAtlas, ImagePlugin
from pybevy.math import UVec2Animation Components
We'll create a custom component, AnimationTimer, to hold a Timer that will
control the speed of our animation.
@component
class AnimationTimer(Component):
def __init__(self, timer: Timer):
self.timer = timerSetup
In our setup system, we:
- Load the sprite sheet image.
- Create a
TextureAtlasLayoutthat describes how the image is divided into frames. Our example sheet has 7 frames in a single row. - Spawn a
Spritewith aTextureAtlascomponent, which links the sprite to the layout and sets the initial frame (index).
def setup(
commands: Commands,
asset_server: AssetServer,
texture_atlas_layouts: ResMut[Assets[TextureAtlasLayout]],
):
commands.spawn(Camera2d())
texture = asset_server.load_image("textures/rpg/chars/gabe/gabe-idle-run.png")
layout = TextureAtlasLayout.from_grid(UVec2(24, 24), 7, 1, None, None)
texture_atlas_layout = texture_atlas_layouts.add(layout)
commands.spawn(
Sprite(
image=texture,
texture_atlas=TextureAtlas(
layout=texture_atlas_layout,
index=1, # Start with the first frame of the run animation
),
),
Transform.from_scale(Vec3.splat(6.0)),
AnimationTimer(Timer(0.1, TimerMode.REPEATING)),
)Animation System
This system runs every frame to update the animation.
- It queries for our
AnimationTimerand theSprite'sTextureAtlas. - It
ticksthe timer with the time that has passed since the last frame. - If the timer has just finished, it advances the
texture_atlas.indexto the next frame, looping back to the start if necessary.
def animate_sprite_system(
time: Res[Time],
query: Query[tuple[Mut[AnimationTimer], Mut[Sprite]]],
):
for timer, sprite in query:
timer.timer.tick(time.delta_secs())
if timer.timer.just_finished():
# The run animation is in frames 1-6
if sprite.texture_atlas.index == 6:
sprite.texture_atlas.index = 1
else:
sprite.texture_atlas.index += 1Running the App
When you run this, you'll see an animated character running in place.
@entrypoint
def main(app: App) -> App:
return (
app
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, animate_sprite_system)
)
if __name__ == "__main__":
main().run()