Skip to content

AnimationGraph——混合与过渡

单个片段只是起点。真实游戏里角色同时播放"走路"和"挥手",或者在"idle"和"walk"之间平滑过渡——这就需要动画图

AnimationGraph 是一个有向无环图(DAG),节点分三种:

  • Clip 节点:叶子节点,持有 AnimationClip 句柄
  • Blend 节点:把子节点按权重线性混合(权重自动归一化)
  • Add 节点:叠加式混合,权重不归一化(适合在主动画上叠加局部动画)

图的根节点(graph.root)始终是一个 Blend 节点。

构建一个混合图

下面的例子让一个球体同时在 X 轴和 Z 轴上做往复运动,两条动画各占 50% 权重,结果是球体沿对角线运动。

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, 4.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    commands.spawn((
        PointLight {
            intensity: 500_000.0,
            ..default()
        },
        Transform::from_xyz(2.0, 5.0, 2.0),
    ));

    let target_name = Name::new("blended");
    let target_id = AnimationTargetId::from_name(&target_name);

    let mut clip_a = AnimationClip::default();
    clip_a.add_curve_to_target(
        target_id,
        AnimatableCurve::new(
            animated_field!(Transform::translation),
            AnimatableKeyframeCurve::new([
                (0.0, Vec3::new(-2.0, 0.0, 0.0)),
                (1.0, Vec3::new(2.0, 0.0, 0.0)),
                (2.0, Vec3::new(-2.0, 0.0, 0.0)),
            ])
            .unwrap(),
        ),
    );

    let mut clip_b = AnimationClip::default();
    clip_b.add_curve_to_target(
        target_id,
        AnimatableCurve::new(
            animated_field!(Transform::translation),
            AnimatableKeyframeCurve::new([
                (0.0, Vec3::new(0.0, 0.0, -2.0)),
                (1.0, Vec3::new(0.0, 0.0, 2.0)),
                (2.0, Vec3::new(0.0, 0.0, -2.0)),
            ])
            .unwrap(),
        ),
    );

    let mut graph = AnimationGraph::new();
    let blend_node = graph.add_blend(0.5, graph.root);
    let node_a = graph.add_clip(clips.add(clip_a), 1.0, graph.root);
    let node_b = graph.add_clip(clips.add(clip_b), 1.0, blend_node);

    let mut player = AnimationPlayer::default();
    player.play(node_a).repeat();
    player.play(node_b).repeat();

    let entity = commands
        .spawn((
            Mesh3d(meshes.add(Sphere::new(0.4))),
            MeshMaterial3d(materials.add(Color::srgb(0.9, 0.3, 0.3))),
            target_name,
            AnimationGraphHandle(graphs.add(graph)),
            player,
        ))
        .id();
    commands
        .entity(entity)
        .insert((target_id, AnimatedBy(entity)));
}

Listing 30-2:两个动画片段按 50/50 混合(examples/listing-30-02.rs)

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

球体沿 XZ 对角线运动——两条动画的叠加效果。

add_blendadd_clip

AnimationGraph::add_blend(weight, parent) 在图里插入一个 Blend 节点。它的 weight 决定这个分支在父节点中的占比。add_clip(handle, weight, parent) 插入一个 Clip 节点。

图 30-1 的结构:

text
Root (blend)
├── Clip A (weight 1.0)
└── Blend (weight 0.5)
    └── Clip B (weight 1.0)

Clip A 直接挂在 Root 下,权重 1.0。Clip B 挂在一个权重 0.5 的 Blend 节点下,所以 Clip B 的有效权重是 0.5。Root 把所有子节点的权重归一化后混合,最终 Clip A 占 2/3、Clip B 占 1/3。

运行时调整权重

图的结构在构建时确定,但节点权重和 ActiveAnimation 的权重都可以在运行时修改:

rust
if let Some(active) = player.animation_mut(node_index) {
    active.set_weight(0.8);
}

AnimationPlayer::animation_mut 返回 Option<&mut ActiveAnimation>,可以调整权重、速度、暂停/恢复等。