播放一个动画片段
Bevy 里播放动画需要四样东西:
AnimationClip——动画资产,存储一组曲线(curve),每条曲线描述某个属性随时间的变化AnimationGraph——动画图,描述多个片段如何组合;最简单的情况是只含一个片段AnimationPlayer——组件,控制播放(开始、暂停、变速、循环等)AnimationGraphHandle——组件,把图的句柄挂到实体上
先看一个最小例子:让一个方块在场景里左右移动并旋转。
use bevy::{
animation::{animated_field, animation_curves::AnimatableCurve, AnimatedBy, AnimationTargetId},
prelude::*,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut clips: ResMut<Assets<AnimationClip>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 3.0, 6.0).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn((
PointLight {
intensity: 500_000.0,
..default()
},
Transform::from_xyz(2.0, 4.0, 2.0),
));
let target_name = Name::new("cube");
let target_id = AnimationTargetId::from_name(&target_name);
let mut clip = AnimationClip::default();
clip.add_curve_to_target(
target_id,
AnimatableCurve::new(
animated_field!(Transform::translation),
AnimatableKeyframeCurve::new([
(0.0, Vec3::new(-1.5, 0.0, 0.0)),
(1.0, Vec3::new(1.5, 0.0, 0.0)),
(2.0, Vec3::new(-1.5, 0.0, 0.0)),
])
.unwrap(),
),
);
clip.add_curve_to_target(
target_id,
AnimatableCurve::new(
animated_field!(Transform::rotation),
AnimatableKeyframeCurve::new([
(0.0, Quat::IDENTITY),
(1.0, Quat::from_rotation_y(std::f32::consts::PI)),
(2.0, Quat::from_rotation_y(std::f32::consts::TAU)),
])
.unwrap(),
),
);
let (graph, node_index) = AnimationGraph::from_clip(clips.add(clip));
let mut player = AnimationPlayer::default();
player.play(node_index).repeat();
let cube_entity = commands
.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::srgb(0.2, 0.7, 0.9))),
target_name,
AnimationGraphHandle(graphs.add(graph)),
player,
))
.id();
commands
.entity(cube_entity)
.insert((target_id, AnimatedBy(cube_entity)));
}Listing 30-1:程序化动画——方块平移加旋转(examples/listing-30-01.rs)
cargo run -p ch30-animation --example listing-30-01方块会一边左右移动一边绕 Y 轴旋转,2 秒一个循环。
曲线是怎么构建的
动画的核心是曲线。AnimatableCurve 把一个"要动画的属性"和一条"值随时间变化的曲线"绑在一起。
属性用 animated_field! 宏选取——它返回一个 AnimatedField,告诉动画系统"去改 Transform 的 translation 字段"。曲线用 AnimatableKeyframeCurve::new 从关键帧列表构建,每个关键帧是 (时间, 值) 对。Bevy 会在关键帧之间自动插值——Vec3 用线性插值,Quat 用球面线性插值(slerp)。
AnimatableCurve::new(
animated_field!(Transform::translation),
AnimatableKeyframeCurve::new([
(0.0, Vec3::new(-1.5, 0.0, 0.0)),
(1.0, Vec3::new(1.5, 0.0, 0.0)),
(2.0, Vec3::new(-1.5, 0.0, 0.0)),
])
.unwrap(),
)一条 AnimationClip 可以包含多条曲线,分别驱动同一个实体的不同属性(比如同时动 translation 和 rotation),也可以驱动不同实体的不同属性。
AnimationTargetId 与 AnimatedBy
动画曲线通过 AnimationTargetId 定位目标实体。这个 ID 本质上是 UUID,由实体的名字路径派生。AnimationTargetId::from_name(&name) 用单个名字生成 ID;AnimationTargetId::from_names([...].iter()) 用层级路径生成——后者在骨骼动画中常用,因为同一根骨头名字可能在不同层级重复。
AnimatedBy(Entity) 组件告诉 Bevy:"这个实体由哪个 AnimationPlayer 驱动"。两者配合,动画系统才能在每帧找到正确的实体并写入属性。
AnimationGraph::from_clip
最简单的图只有一个片段。AnimationGraph::from_clip(handle) 返回 (graph, node_index)——图本身和片段在图中的节点索引。节点索引就是 AnimationPlayer 用来标识"播哪个动画"的钥匙。
let (graph, node_index) = AnimationGraph::from_clip(clips.add(clip));
let mut player = AnimationPlayer::default();
player.play(node_index).repeat();play 让动画开始播放,repeat 设为循环。repeat() 等价于 set_repeat(RepeatAnimation::Forever)。还有 RepeatAnimation::Never(播一次停)和 RepeatAnimation::Count(n)(播 n 次)。
循环动画的首尾衔接
注意 Listing 30-1 里,关键帧列表的最后一个值和第一个值相同(Vec3::new(-1.5, 0.0, 0.0) 出现了两次)。这是循环动画的常见做法——如果首尾不同,循环时会产生跳变。