Bevy 中的动画:全局视角

Hacker News Top 工具

摘要

本文详细介绍了如何在 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); ``` 此时我们的心智模型大致如下: ![图示:左侧蓝色方框“3D模型”,右侧红色方框“动画数据”,中间黄色椭圆“播放”连接两者](此处保留原图占位符) 实际上这与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模型通常不是作为单一实体生成,而是以实体层级结构存在: ![图示:蓝色方框“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`对应的动画,并驱动所属模型进行动画播放。 以下是我此刻的理解示意图: ![图示:蓝色方框内为实体层级结构,黄色组件为AnimationPlayer,同实体新增红色组件AnimationGraphHandle。右侧红色方框“动画数据”包含AnimationGraph子框与NodeIndex子框,红色箭头连接AnimationGraphHandle与AnimationGraph框,椭圆“播放”连接AnimationPlayer与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(); ```

相似文章

Bevy 0.19

Lobsters Hottest

Bevy 0.19 是 Rust 游戏引擎的一个重要更新,引入了下一代场景(BSN 格式)、通过更多 GPU 工作实现更快的渲染、接触阴影、新的 UI 组件、文本输入支持、后处理效果等等。

Bevy 六周年

Lobsters Hottest

Bevy 创始人对引擎六周年的回顾,重点提及 Bevy 0.17 和 0.18 版本发布、新的指标系统、周边商店和社区 Jam 等里程碑,并展望未来一年。

Animos App

Product Hunt

Animos App 是一个展示动态设计的工具。

它打破了一个村庄:Bevy的六周年

Lobsters Hottest

本文回顾了Bevy游戏引擎的六周年,重点介绍了最近的技术改进,如BSN宏和UI进步,同时探讨了社区对一项新AI政策的困扰,该政策允许在贡献中使用更多LLM,引发了关于开源贡献伦理的担忧。