Animation in Bevy: The Big Picture

Hacker News Top Tools

Summary

This article explains how to work with animations in the Bevy game engine, providing a step-by-step guide to understand the animation system and components like AnimationPlayer.

No content available
Original Article
View Cached Full Text

Cached at: 09/08/26, 09:43 PM

# Grégoire Locqueville | Animation in Bevy: The Big Picture Source: [http://glocq.com/en/blog/20260827/](http://glocq.com/en/blog/20260827/) *August 27, 2026* So you just downloaded an animated 3D character`\.glb`file on a free assets website\. You have a basic Bevy application running\. Now you want to spawn your character in your Bevy app\. Easy enough, there's probably a function for that\. You look up the basic[Animated Mesh example](https://bevy.org/examples/animation/animated-mesh/)on Bevy's website\.\.\. and it quickly becomes apparent that things are not as simple as you've hoped\. There's a lot of setting things up, animation\-related types whose exact purpose is not obvious\.\.\. You might be able to tweak the examples to fit your needs reasonably fast, but actually forming a mental model of how animation works in Bevy, based on the examples, is going to take some serious pondering\. This post is what I wish I had when trying to understand animation in Bevy a couple weeks ago\. I start by building up, step by step, enough of a mental model to deal with basic animation in Bevy\. Then I walk you through the official example, explaining how it relates to that mental model, and also holding your hand a bit when encountering methods you might not have encountered yet\. If you are comfortable with the basics of Bevy’s ECS, you’re in this post’s target audience :\) ## First Intuition Let’s forget about Bevy and ECS for a second, and think \(very abstractly\) about what it takes to get an animated 3D model to move\. You need two ingredients: 1. A spawned**3D model**\(or some kind of reference to one\), 2. An**animation**to be played \(or some kind of reference to one\)\. So at first, the way you might expect to play an animation is something like that: ``` my_model.play_animation(my_animation); ``` Here's what our mental model looks like so far: ![A diagram with a blue box titled 3D Model on the left, a red box titled animation data on the right, and a yellow oval titled play connecting both boxes](http://glocq.com/images/animation_intuition.svg) Now this is actually somewhat close to the way an actual Bevy function works, namely, the[`play`](https://docs.rs/bevy/latest/bevy/animation/struct.AnimationPlayer.html#method.play)method from the`AnimationPlayer`type… or at least it would be, if its`self`actually referred to a 3D model, and its`animation`argument actually referred to an animation\. Right now, though, it’s not clear that they do\. But let’s look in more detail into the way our two ingredients are represented in Bevy, and maybe we can reconcile our intuition with the way the`play`function works\. ## AnimationPlayer: A way to control a 3D model’s animations Say you spawn a 3D model from a`\.glb`file\. Ideally, you’d like to refer to it using the ID of the spawned entity, except 3D models are generally not spawned as a single entity, but rather as a hierarchy of entities: ![A diagram with a blue box titled 3D Model enclosing a tree of boxes representing entities, each containing a couple boxes representing components](http://glocq.com/images/model_hierarchy.svg) To animate your model, you need to refer to it some other way\. Bevy has a mechanism to do just that — refer to a 3D model for animation — , in the form of the[`AnimationPlayer`](https://docs.rs/bevy/latest/bevy/animation/struct.AnimationPlayer.html)type\.`AnimationPlayer`is a`Component`that is automatically inserted in an`Entity`…*somewhere*in the entity hierarchy corresponding to an animatable 3D model, when that model is spawned\. ![A diagram representing the same hierarchy of entities, except one of the components is yellow and represents the animation player component](http://glocq.com/images/animation_player.svg) Once you have gotten a hold of an`AnimationPlayer`, you can tell it to play/pause an animation, access the one currently playing… but that’s assuming you have animations in the first place\! Let’s focus on those now\. ## AnimationGraph: A way to store and combine animations The way animations are represented in Bevy is a little complex, because what you’ll generally manipulate is not single animations, but instances of a data structure able to store several animations at once and combine them with one another\. That structure is the[`AnimationGraph`](https://docs.rs/bevy/latest/bevy/animation/graph/struct.AnimationGraph.html)\. I won’t get into the details of how`AnimationGraph`s are used here; once you’re comfortable with simple animations, you can learn more about general animation graphs with the[the Animation Graph example](https://bevy.org/examples/animation/animation-graph/)\. For now we’ll focus on cases where our graph contains**only one animation**that we’re interested in\. In those cases, the data that identifies your animation will be: 1. \(A reference to\) an`AnimationGraph`, 2. An identifier for where your animation actually sits inside the graph — that's what the[`NodeIndex`](https://docs.rs/petgraph/0.8.3/petgraph/graph/struct.NodeIndex.html)type is for\. ## Putting it Together Ok, say you have an`AnimationPlayer`, as well as an`AnimationGraph`and a`NodeIndex`\. Here’s how you connect them to play your animation: 1. Insert a reference to your`AnimationGraph`as a component on the same entity as your`AnimationPlayer`\. That’s exactly what the[`AnimationGraphHandle`](https://docs.rs/bevy/latest/bevy/animation/graph/struct.AnimationGraphHandle.html)is for: it holds a`Handle`to an`AnimationGraph`, and it implements the`Component`trait\. 2. Call`my\_animation\_player\.play\(my\_node\_index\);`\. That’s it\! Upon that call,`my\_animation\_player`knows to look for the`AnimationGraph`that’s a component on the same entity as itself, it looks for the animation with index`NodeIndex`in that graph, and animates the model it belongs to\. Here's how I picture things at this point: ![Same diagram representing an entity hierarchy enclosed in a blue box, with one of the components colored yellow to represent the AnimationPlayer component, but now there's an additional component in the same entity as the AnimationPlayer. That new component is colored red and stands for the AnimationGraphHandle component. On the right of all that, there's a red box titled Animation Data, containing a subbox titled AnimationGraph and a subbox named NodeIndex. A red arrow connects the AnimationGraphHandle to the AnimationGraph box. An oval titled play connects the AnimationPlayer component with the NodeIndex box.](http://glocq.com/images/animation_full.svg) Now let’s look at the example code again\. ## A Walk through the Example Code In this section, I’ll simply walk through[`Animated Mesh`](https://bevy.org/examples/animation/animated-mesh/), from Bevy’s official examples, explaining how it relates to what I said above along the way\. I’ll mostly go in chronological order, though I may reorder or even skip over some sections\. My aim is to provide an explanation that’s a little more in\-depth than the existing inline comments \(which are already pretty thorough as far as comments go\!\), and connect it to what we’ve talked about in the abstract until now\. The first lines are standard Bevy stuff; just note that we add the system`setup\_mesh\_and\_animation`, where we’ll put the animation logic\. Let’s look at that system now, which runs at startup\. First thing we do is extract animation data from our`\.glb`file: ``` let (graph, index) = AnimationGraph::from_clip( asset_server.load(GltfAssetLabel::Animation(2).from_asset(GLTF_PATH)), ); ``` There’s some boilerplate involved to load the animation clip; you don’t need to understand the detail of each function call, however you can note a few things: - The[`GltfAssetLabel::Animation`](https://docs.rs/bevy/latest/bevy/gltf/enum.GltfAssetLabel.html)constructor takes an integer that refers to your animation inside the`\.glb`file\. If you don’t know the exact structure of your file, you’ll have to guess: try putting in numbers like 0, 1, 2, and see if your animation shows up\. - The[`from\_clip`](https://docs.rs/bevy/latest/bevy/prelude/struct.AnimationGraph.html#method.from_clip)method returns both an`AnimationGraph`and a`NodeIndex`in that graph — exactly the data you need to refer to an animation\. Then we add the graph we get to the asset store, and keep a`Handle`to it: ``` let graph_handle = graphs.add(graph); ``` We also load our mesh; like for the graph, there’s some boilerplate involved which you don’t need to completely understand right away: ``` let mesh_scene = WorldAssetRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(GLTF_PATH))); ``` It’s enough to know that the`mesh\_scene`you get can be spawned right away with the[`spawn`](https://docs.rs/bevy/latest/bevy/prelude/struct.Commands.html#method.spawn)method\. Here, however, for convenience, we bundle it with a custom component,`animation\_to\_play`\(which has custom type`AnimationToPlay`, defined earlier in the example\)\.`animation\_to\_play`contains the two pieces of animation data we will need, namely the animation graph handle and the graph index: ``` commands .spawn((animation_to_play, mesh_scene)) ``` And then we pass control to the`play\_animation\_when\_ready`system once things are properly spawned: ``` .observe(play_animation_when_ready); ``` Because it is called on the result of a`spawn`call, this`observe`call injects the spawned entity to the system it is given as argument\. In other words, the root entity of our mesh scene, which we just spawned, can be accessed, as we will see, from the body of`play\_animation\_when\_ready`, in the form of the`entity`field of the`scene\_ready`argument\. The remaining tasks are a little less straightforward than what we’ve done so far\. We need to: 1. Locate the entity that has the`AnimationPlayer`component, 2. Add a handle to our animation graph as a component to it, 3. Tell the animation player to play the animation we want\. We’ve got a problem with step 1, though: we loaded our mesh scene from a`\.glb`file, which spawns a hierarchy of entities; the animation player will have been attached to*some*entity in that hierarchy, but we don’t know which\! Maybe if we knew very well how our`\.glb`file is structured, and how GLTFs are represented in Bevy’s ECS, we could infer what entity has the animation player…? Eh, forget that, we’ll just manually visit all descendants of the mesh scene root, and check whether they have an`AnimationPlayer`component\. We start by getting the root entity of our mesh scene hierarchy\. As mentioned earlier, this is injected into this system as the`entity`field of the`scene\_ready`argument\. That entity should have a component of type`AnimationToPlay`containing the animation data we need, so we can directly[`get`](https://docs.rs/bevy/latest/bevy/ecs/prelude/struct.Query.html#method.get)it: ``` if let Ok(animation_to_play) = animations_to_play.get(scene_ready.entity) { ``` \(Don’t confuse`animations\_to\_play`, a`Query`for components of type`AnimationToPlay`, and`animation\_to\_play`, the actual`AnimationToPlay`that we get by looking at our entity through the prism of that query using the[`get`](https://docs.rs/bevy/latest/bevy/ecs/prelude/struct.Query.html#method.get)method\.\) We have the scene root \(as well as our animation data\), but the scene root is not where the`AnimationPlayer`component lives: all we know is that it will have been attached to*some*entity in our scene hierarchy, in a way that’s dependent on our`\.glb`file and the specifics of how Bevy instantiates those files in its ECS\. We don’t need to know about all that, though: we can just go through all entities in the scene \(that is, all descendants of the root entity\), and check whether they have an`AnimationPlayer`component: ``` for child in children.iter_descendants(scene_ready.entity) { if let Ok(mut player) = players.get_mut(child) { ``` \(In case you’re not familiar with[`iter\_descendants`](https://docs.rs/bevy/latest/bevy/prelude/struct.Query.html#method.iter_descendants), it’s a method which you must call on a`Query<&Children\>`\(though you can replace`Children`with another`RelationshipTarget`if you’re interested in another kind of hierarchy\) and which does what you’d expect, that is, iterate over all entities that descend from the argument you give it\.\) Phew\! We’ve done the hard part, find the entity with the animation player\. Now we can add the animation graph to that entity \(the example tells the player to play the animation before the graph is added, which I guess works, but makes less intuitive sense\): First we get the[`EntityCommands`](https://docs.rs/bevy/latest/bevy/prelude/struct.EntityCommands.html)corresponding to the animation player entity, which will allow us to do stuff like add a component to it: ``` commands.entity(child) ``` and then we add to it, as a component, the handle to the animation graph contained in`animation\_to\_play`: ``` .insert(AnimationGraphHandle(animation_to_play.graph_handle.clone())); ``` Finally, we can tell our player to start playing the animation at the index specified in`animation\_to\_play`, and also tell it to loop: ``` player.play(animation_to_play.index).repeat(); ```

Similar Articles

Bevy 0.19

Lobsters Hottest

Bevy 0.19, a major update to the Rust game engine, introduces next-generation scenes (BSN format), faster rendering with more GPU work, contact shadows, new UI widgets, text input support, post-processing effects, and more.

Bevy's Sixth Birthday

Lobsters Hottest

Bevy creator reflects on the engine's sixth anniversary, highlighting milestones like Bevy 0.17 and 0.18 releases, a new metrics system, merchandise store, and community jam, while looking forward to the next year.

Animos App

Product Hunt

Animos App is a tool for showcasing designs in motion.

It Breaks a Village: Bevy's 6th Birthday

Lobsters Hottest

The article reflects on the 6th anniversary of the Bevy game engine, highlighting recent technical improvements like the BSN macro and UI advancements, while addressing community distress over a new AI policy that permits more LLM usage in contributions, raising concerns about open-source contribution ethics.