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_blend 与 add_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>,可以调整权重、速度、暂停/恢复等。