Skip to content

播放一个动画片段

Bevy 里播放动画需要四样东西:

  1. AnimationClip——动画资产,存储一组曲线(curve),每条曲线描述某个属性随时间的变化
  2. AnimationGraph——动画图,描述多个片段如何组合;最简单的情况是只含一个片段
  3. AnimationPlayer——组件,控制播放(开始、暂停、变速、循环等)
  4. AnimationGraphHandle——组件,把图的句柄挂到实体上

先看一个最小例子:让一个方块在场景里左右移动并旋转。

rust
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)

console
cargo run -p ch30-animation --example listing-30-01

方块会一边左右移动一边绕 Y 轴旋转,2 秒一个循环。

曲线是怎么构建的

动画的核心是曲线AnimatableCurve 把一个"要动画的属性"和一条"值随时间变化的曲线"绑在一起。

属性用 animated_field! 宏选取——它返回一个 AnimatedField,告诉动画系统"去改 Transformtranslation 字段"。曲线用 AnimatableKeyframeCurve::new 从关键帧列表构建,每个关键帧是 (时间, 值) 对。Bevy 会在关键帧之间自动插值——Vec3 用线性插值,Quat 用球面线性插值(slerp)。

rust
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 可以包含多条曲线,分别驱动同一个实体的不同属性(比如同时动 translationrotation),也可以驱动不同实体的不同属性。

AnimationTargetIdAnimatedBy

动画曲线通过 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 用来标识"播哪个动画"的钥匙。

rust
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) 出现了两次)。这是循环动画的常见做法——如果首尾不同,循环时会产生跳变。