Bevy 中的动画:全局视角
摘要
本文详细介绍了如何在 Bevy 游戏引擎中使用动画功能,并提供了分步指南,帮助您理解动画系统及 AnimationPlayer 等核心组件。
暂无内容
查看缓存全文
缓存时间: 2026/09/08 21:43
# Grégoire Locqueville | Bevy中的动画:全景解析
来源:http://glocq.com/en/blog/20260827/
*2026年8月27日*
假设你刚在免费资源网站下载了一个带动画的3D角色`.glb`文件,且已有一个基础的Bevy应用运行起来。现在你想将这个角色添加到你的应用中——这似乎很简单,肯定有现成的函数可用。于是你查阅了Bevy官网的《基础动画网格》示例(https://bevy.org/examples/animation/animated-mesh/)……但很快你会发现事情没那么简单。需要大量配置,动画相关类型的目的也不明确……虽然你或许能快速调整示例以适配需求,但若要基于这些示例真正建立对Bevy动画工作原理的心智模型,则需要深入思考。
本文正是我几周前试图理解Bevy动画时所期望的指南。我将首先逐步构建足以处理基础动画的心智模型,然后带你剖析官方示例,解释其如何契合该模型,并在遇到可能不熟悉的方法时提供引导。
如果你熟悉Bevy ECS的基础知识,那么你就是本文的目标读者:)
## 初始直觉
让我们暂时抛开Bevy和ECS,抽象思考一下:要让3D模型动起来需要什么?需要两个要素:
1. 一个已生成的**3D模型**(或其引用)
2. 一段需要播放的**动画**(或其引用)
因此,最初你可能会设想播放动画的方式是:
```
my_model.play_animation(my_animation);
```
此时我们的心智模型大致如下:

实际上这与Bevy函数的工作方式有相似之处——比如`AnimationPlayer`(https://docs.rs/bevy/latest/bevy/animation/struct.AnimationPlayer.html#method.play)类型的`play`方法。但前提是它的`self`确实对应3D模型,且`animation`参数确实对应动画。而目前情况并非如此。接下来让我们深入分析这两个要素在Bevy中的具体表现,或许就能调和直觉与`play`函数的工作方式。
## AnimationPlayer:控制3D模型动画的组件
假设你从`.glb`文件生成了一个3D模型。理想情况下,你可以通过生成的实体ID引用它,但3D模型通常不是作为单一实体生成,而是以实体层级结构存在:

要驱动模型动画,必须通过其他方式引用它。
Bevy为此提供了专门机制——以`AnimationPlayer`(https://docs.rs/bevy/latest/bevy/animation/struct.AnimationPlayer.html)组件形式实现。当可动画的3D模型生成时,该组件会自动插入到对应实体层级中的*某个*实体上:

获取到`AnimationPlayer`后,你就可以控制播放/暂停动画、访问当前动画……但这要求你必须先有动画!现在让我们聚焦于此。
## AnimationGraph:存储与组合动画的数据结构
Bevy中的动画表示相对复杂,因为你通常操作的不是单个动画,而是能够存储多个动画并将其组合的数据结构实例——即`AnimationGraph`(https://docs.rs/bevy/latest/bevy/animation/graph/struct.AnimationGraph.html)。
本文不会深入`AnimationGraph`的详细用法;当你熟悉基础动画后,可以通过《动画图示例》(https://bevy.org/examples/animation/animation-graph/)进一步学习。现在我们将聚焦于仅包含**单个目标动画**的情况。此时标识动画的数据包括:
1. (对)`AnimationGraph`的引用
2. 动画在图中的位置标识符——这就是`NodeIndex`(https://docs.rs/petgraph/0.8.3/petgraph/graph/struct.NodeIndex.html)类型的作用
## 整合应用
假设你已拥有`AnimationPlayer`以及`AnimationGraph`和`NodeIndex`。以下是连接它们播放动画的步骤:
1. 将`AnimationGraph`引用作为组件添加到与`AnimationPlayer`相同的实体上。`AnimationGraphHandle`(https://docs.rs/bevy/latest/bevy/animation/graph/struct.AnimationGraphHandle.html)正是为此设计:它持有`AnimationGraph`的`Handle`并实现了`Component` trait。
2. 调用`my_animation_player.play(my_node_index);`
就这样!调用后,`my_animation_player`会查找同实体上的`AnimationGraph`组件,在图中定位`NodeIndex`对应的动画,并驱动所属模型进行动画播放。
以下是我此刻的理解示意图:

现在让我们重新审视示例代码。
## 示例代码解析
本节将逐步解析Bevy官方示例《动画网格》(https://bevy.org/examples/animation/animated-mesh/),并关联前文所述模型。将按时间顺序讲解,必要时会调整或跳过部分内容。目标是提供比现有注释(本身已相当详尽)更深入的解析,并与前文抽象概念建立联系。
开篇是标准Bevy配置;注意我们添加了`setup_mesh_and_animation`系统,动画逻辑将置于此处。现在查看该启动时运行的系统,首先从`.glb`文件提取动画数据:
```
let (graph, index) = AnimationGraph::from_clip(
asset_server.load(GltfAssetLabel::Animation(2).from_asset(GLTF_PATH)),
);
```
加载动画剪辑有些样板代码;无需完全理解每个函数调用,但请注意:
- `GltfAssetLabel::Animation`(https://docs.rs/bevy/latest/bevy/gltf/enum.GltfAssetLabel.html)构造器接收一个整数参数,指向`.glb`文件中的动画。若不明确文件结构,可尝试输入0、1、2等数字测试动画是否出现。
- `from_clip`(https://docs.rs/bevy/latest/bevy/prelude/struct.AnimationGraph.html#method.from_clip)方法同时返回`AnimationGraph`和对应的`NodeIndex`——这正是引用动画所需的数据。
随后将获得的图添加到资源存储并保留其`Handle`:
```
let graph_handle = graphs.add(graph);
```
同时加载网格模型;与图加载类似,存在一些暂时无需完全理解的样板代码:
```
let mesh_scene =
WorldAssetRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(GLTF_PATH)));
```
只需知道获得的`mesh_scene`可通过`spawn`(https://docs.rs/bevy/latest/bevy/prelude/struct.Commands.html#method.spawn)方法直接生成。此处为方便起见,我们将其与自定义组件`animation_to_play`(即示例中提前定义的`AnimationToPlay`类型)捆绑。`animation_to_play`包含所需两项动画数据:动画图句柄和图索引:
```
commands
.spawn((animation_to_play, mesh_scene))
```
随后当生成完成后,控制权将传递给`play_animation_when_ready`系统:
```
.observe(play_animation_when_ready);
```
由于该调用作用于`spawn`返回结果,`observe`会将生成的实体注入给定系统。换言之,刚刚生成的网格场景根实体可通过`play_animation_when_ready`中的`scene_ready`参数的`entity`字段访问。
剩余任务比已完成步骤稍复杂。我们需要:
1. 定位拥有`AnimationPlayer`组件的实体
2. 将动画图句柄作为组件添加到该实体
3. 告知动画播放器播放目标动画
但步骤1存在问题:我们从`.glb`文件加载网格场景,这会生成实体层级结构;`AnimationPlayer`会附着在*某个*实体上,但具体是哪个实体未知!若非常了解`.glb`文件结构及GLTF在Bevy ECS中的表示方式,或许能推断出拥有动画播放器的实体……不过算了,我们将手动遍历网格场景根实体的所有后代,检查是否包含`AnimationPlayer`组件。
首先获取网格场景层级的根实体。如前所述,它通过`scene_ready`参数的`entity`字段注入系统。该实体应包含`AnimationToPlay`组件(其中存储所需动画数据),因此可直接通过`get`(https://docs.rs/bevy/latest/bevy/ecs/prelude/struct.Query.html#method.get)获取:
```
if let Ok(animation_to_play) = animations_to_play.get(scene_ready.entity) {
```
(注意区分`animations_to_play`——用于查询`AnimationToPlay`类型的`Query`,与`animation_to_play`——通过查询视角在实体上获取的实际`AnimationToPlay`实例)
获得场景根实体(及动画数据)后,需知根实体并非`AnimationPlayer`组件所在位置:仅知其附着在层级中*某个*实体上,具体位置取决于`.glb`文件及Bevy实例化这些文件的方式。不过无需深究这些:只需遍历场景所有实体(即根实体的所有后代),检查是否存在`AnimationPlayer`组件:
```
for child in children.iter_descendants(scene_ready.entity) {
if let Ok(mut player) = players.get_mut(child) {
```
(若不熟悉`iter_descendants`(https://docs.rs/bevy/latest/bevy/prelude/struct.Query.html#method.iter_descendants),它是一个需在`Query<&Children>`上调用的方法(可用其他`RelationshipTarget`替代`Children`以遍历其他层级),功能如其名:迭代给定实体的所有后代。)
呼!我们完成了最困难的部分——找到拥有动画播放器的实体。现在可以将动画图添加到该实体(示例在添加图之前就让播放器播放动画,虽然可行但不够直观):
首先获取对应动画播放器实体的`EntityCommands`(https://docs.rs/bevy/latest/bevy/prelude/struct.EntityCommands.html),以便添加组件:
```
commands.entity(child)
```
然后将`animation_to_play`中的动画图句柄作为组件添加:
```
.insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
```
最后,告知播放器开始播放`animation_to_play`中指定索引的动画,并设置循环:
```
player.play(animation_to_play.index).repeat();
```
相似文章
@emilkowalski: 要想让AI生成好的动画,你需要善于描述你的需求:- "将列表中的项目错开" - "使…
Emil Kowalski 分享了一个动作词汇网站,帮助开发者和设计师在向AI描述动画时更精确。该词汇表涵盖了入场、出场、排序、变换、过渡和滚动触发的运动。
Bevy 0.19
Bevy 0.19 是 Rust 游戏引擎的一个重要更新,引入了下一代场景(BSN 格式)、通过更多 GPU 工作实现更快的渲染、接触阴影、新的 UI 组件、文本输入支持、后处理效果等等。
Bevy 六周年
Bevy 创始人对引擎六周年的回顾,重点提及 Bevy 0.17 和 0.18 版本发布、新的指标系统、周边商店和社区 Jam 等里程碑,并展望未来一年。
Animos App
Animos App 是一个展示动态设计的工具。
它打破了一个村庄:Bevy的六周年
本文回顾了Bevy游戏引擎的六周年,重点介绍了最近的技术改进,如BSN宏和UI进步,同时探讨了社区对一项新AI政策的困扰,该政策允许在贡献中使用更多LLM,引发了关于开源贡献伦理的担忧。