动画基础

概述

学习目标

  • 理解 Bevy 动画系统的基本概念
  • 掌握动画网格的创建和播放
  • 了解动画变换的使用
  • 学会使用动画播放器和动画图

前置知识要求

  • Bevy 快速入门
  • ECS 基础
  • 3D 开发基础
  • 资源管理基础

核心概念

什么是动画系统?

动画系统是 Bevy 中用于创建和播放动画的功能。Bevy 的动画系统支持网格动画、变换动画、UI 动画等多种类型的动画。

为什么需要动画系统?

  1. 视觉效果:动画可以增强游戏的视觉效果
  2. 用户体验:动画可以改善用户体验
  3. 游戏性:动画是游戏性的重要组成部分
  4. 艺术表现:动画可以增强艺术表现力

动画系统的核心组件

Bevy 动画系统包含以下核心组件:

  • AnimationClip:动画片段,包含动画数据
  • AnimationGraph:动画图,用于组织和管理动画
  • AnimationPlayer:动画播放器,用于播放动画
  • AnimationTarget:动画目标,用于指定动画作用的对象

基础用法

动画网格

创建和播放动画网格。

源代码文件bevy/examples/animation/animated_mesh.rs

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
use bevy::{light::CascadeShadowConfigBuilder, prelude::*, scene::SceneInstanceReady};

// 包含网格和动画的示例资源
const GLTF_PATH: &str = "models/animated/Fox.glb";

fn main() {
App::new()
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 2000.,
..default()
})
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup_mesh_and_animation)
.add_systems(Startup, setup_camera_and_environment)
.run();
}

// 一个存储我们要播放的动画引用的组件。这在我们开始加载网格时创建
// (参见 `setup_mesh_and_animation`),并在网格生成时读取(参见 `play_animation_when_ready`)。
#[derive(Component)]
struct AnimationToPlay {
graph_handle: Handle<AnimationGraph>,
index: AnimationNodeIndex,
}

fn setup_mesh_and_animation(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
// 创建一个包含单个动画的动画图。我们想要示例资源中的"run"动画,其索引为二。
let (graph, index) = AnimationGraph::from_clip(
asset_server.load(GltfAssetLabel::Animation(2).from_asset(GLTF_PATH)),
);

// 将动画图存储为资源。
let graph_handle = graphs.add(graph);

// 创建一个存储我们动画引用的组件。
let animation_to_play = AnimationToPlay {
graph_handle,
index,
};

// 开始将资源作为场景加载,并将其引用存储在 SceneRoot 组件中。
// 此组件将在资源加载后自动生成包含我们网格的场景。
let mesh_scene = SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(GLTF_PATH)));

// 生成一个包含我们组件的实体,并将其连接到一个观察者,
// 该观察者将在场景加载并生成时触发。
commands
.spawn((animation_to_play, mesh_scene))
.observe(play_animation_when_ready);
}

fn play_animation_when_ready(
scene_ready: On<SceneInstanceReady>,
mut commands: Commands,
children: Query<&Children>,
animations_to_play: Query<&AnimationToPlay>,
mut players: Query<&mut AnimationPlayer>,
) {
// 我们在 `setup_mesh_and_animation` 中生成的实体是触发器的目标。
// 首先找到我们添加到该实体的 AnimationToPlay 组件。
if let Ok(animation_to_play) = animations_to_play.get(scene_ready.entity) {
// SceneRoot 组件将场景生成为我们实体的子实体的层次结构。
// 由于资源包含蒙皮网格和动画,它还会生成一个动画播放器组件。
// 搜索我们实体的后代以找到动画播放器。
for child in children.iter_descendants(scene_ready.entity) {
if let Ok(mut player) = players.get_mut(child) {
// 告诉动画播放器开始动画并保持重复播放。
//
// 如果你想尝试停止和切换动画,请参阅 `animated_mesh_control.rs` 示例。
player.play(animation_to_play.index).repeat();

// 添加动画图。这只需要执行一次即可将动画播放器连接到网格。
commands
.entity(child)
.insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
}
}
}
}

关键要点

  • 使用 AnimationGraph::from_clip 从动画片段创建动画图
  • 使用 AnimationPlayer 播放动画
  • 使用 AnimationGraphHandle 将动画图连接到实体
  • 使用 SceneInstanceReady 事件在场景加载后播放动画

说明
动画网格是动画系统的基础。通过加载包含动画的 glTF 文件,可以创建和播放动画网格。

动画变换

创建和播放动画变换。

源代码文件bevy/examples/animation/animated_transform.rs

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use bevy::{
animation::{animated_field, AnimationTarget, AnimationTargetId},
prelude::*,
};

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 150.0,
..default()
})
.add_systems(Startup, setup)
.run();
}

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut animations: ResMut<Assets<AnimationClip>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
// 相机
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));

// 光源
commands.spawn((
PointLight {
intensity: 500_000.0,
..default()
},
Transform::from_xyz(0.0, 2.5, 0.0),
));

// 让我们使用 `Name` 组件来定位实体。我们可以使用任何我们喜欢的东西,
// 但名称很方便。
let planet = Name::new("planet");
let orbit_controller = Name::new("orbit_controller");
let satellite = Name::new("satellite");

// 创建动画
let mut animation = AnimationClip::default();
// 曲线可以修改变换的单个部分:这里,平移。
let planet_animation_target_id = AnimationTargetId::from_name(&planet);
animation.add_curve_to_target(
planet_animation_target_id,
AnimatableCurve::new(
animated_field!(Transform::translation),
UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([
Vec3::new(1.0, 0.0, 1.0),
Vec3::new(-1.0, 0.0, 1.0),
Vec3::new(-1.0, 0.0, -1.0),
Vec3::new(1.0, 0.0, -1.0),
// 如果需要无缝循环,最后一帧应该与第一帧相同
Vec3::new(1.0, 0.0, 1.0),
]))
.expect("应该能够构建平移曲线,因为我们传入有效的样本"),
),
);
// 或者它可以修改变换的旋转。
// 为了找到要修改的实体,将遍历层次结构,在每个级别查找具有正确名称的实体。
let orbit_controller_animation_target_id =
AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter());
animation.add_curve_to_target(
orbit_controller_animation_target_id,
AnimatableCurve::new(
animated_field!(Transform::rotation),
UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([
Quat::IDENTITY,
Quat::from_axis_angle(Vec3::Y, PI / 2.),
Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
Quat::IDENTITY,
]))
.expect("无法构建旋转曲线"),
),
);

// 创建动画图
let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation));

// 创建动画播放器,并设置为重复播放
let mut player = AnimationPlayer::default();
player.play(animation_index).repeat();

// 创建将被动画化的场景
// 第一个实体是行星
let planet_entity = commands
.spawn((
Mesh3d(meshes.add(Sphere::default())),
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
// 添加动画图和播放器
planet,
AnimationGraphHandle(graphs.add(graph)),
player,
))
.id();
commands.entity(planet_entity).insert((
AnimationTarget {
id: planet_animation_target_id,
player: planet_entity,
},
children![(
Transform::default(),
Visibility::default(),
orbit_controller,
AnimationTarget {
id: orbit_controller_animation_target_id,
player: planet_entity,
},
children![(
Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))),
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.9, 0.3))),
Transform::from_xyz(1.5, 0.0, 0.0),
AnimationTarget {
id: satellite_animation_target_id,
player: planet_entity,
},
satellite,
)],
)],
));
}

关键要点

  • 使用 AnimationClip 创建动画片段
  • 使用 AnimatableCurve 创建动画曲线
  • 使用 animated_field! 宏指定动画字段
  • 使用 AnimationTarget 指定动画目标

说明
动画变换是动画系统的重要功能。通过创建动画曲线,可以动画化实体的变换属性。

进阶用法

动画播放器

使用动画播放器控制动画播放。

关键信息

  • 使用 AnimationPlayer::play() 播放动画
  • 使用 .repeat() 重复播放动画
  • 使用 .once() 播放一次动画
  • 使用 .pause() 暂停动画
  • 使用 .resume() 恢复动画

说明
动画播放器是动画系统的核心组件。通过使用动画播放器,可以控制动画的播放状态。

实际应用

在游戏开发中的应用场景

动画系统在游戏开发中有广泛的应用:

  1. 角色动画:创建角色行走、跑步、跳跃等动画
  2. 物体动画:创建物体移动、旋转、缩放等动画
  3. UI 动画:创建 UI 元素的动画效果
  4. 特效动画:创建特效的动画效果

常见问题

问题 1:如何加载动画网格?

解决方案

  • 使用 AssetServer 加载 glTF 文件
  • 使用 GltfAssetLabel::Animation 加载动画
  • 使用 GltfAssetLabel::Scene 加载场景
  • 使用 SceneInstanceReady 事件在场景加载后播放动画

问题 2:如何创建动画变换?

解决方案

  • 使用 AnimationClip 创建动画片段
  • 使用 AnimatableCurve 创建动画曲线
  • 使用 animated_field! 宏指定动画字段
  • 使用 AnimationTarget 指定动画目标

问题 3:如何控制动画播放?

解决方案

  • 使用 AnimationPlayer::play() 播放动画
  • 使用 .repeat() 重复播放动画
  • 使用 .pause() 暂停动画
  • 使用 .resume() 恢复动画

性能考虑

  1. 动画图:使用动画图组织和管理动画
  2. 动画目标:使用动画目标指定动画作用的对象
  3. 动画播放器:合理使用动画播放器控制动画播放

相关资源

相关源代码文件

  • bevy/examples/animation/animated_mesh.rs - 动画网格示例
  • bevy/examples/animation/animated_transform.rs - 动画变换示例

官方文档链接

进一步学习建议

  • 学习动画进阶,了解动画图、动画事件等高级功能
  • 学习 UI 动画,了解 UI 动画的创建和使用

索引返回上级目录