Skip to content

动画事件

动画不只是视觉变化——角色的脚步声、攻击判定帧、特效触发点,都需要在动画的特定时刻执行逻辑。Bevy 的动画事件机制就是为此设计的。

定义动画事件

动画事件是一个实现了 AnimationEvent trait 的类型。最简单的方式是 derive:

rust
#[derive(AnimationEvent, Clone)]
struct Footstep {
    foot: String,
}

AnimationEvent 要求类型同时满足 Clone + Event,derive 宏自动处理这些约束。

注册事件到时间线

AnimationClip::add_event(time, event) 把事件绑定到动画的某个时间点:

rust
clip.add_event(0.0, Footstep { foot: "left".into() });
clip.add_event(2.0, Footstep { foot: "right".into() });

当动画播放到指定时间时,Bevy 会自动触发该事件。事件触发时,AnimationEventTrigger 会提供触发事件的实体信息。

监听事件

用 Bevy 的 Observer 机制监听动画事件:

rust
fn on_footstep(event: On<Footstep>) {
    info!("Footstep: {}", event.foot);
}

App::new()
    .add_observer(on_footstep)
    .run();

完整的播放与事件触发示例:

rust
use bevy::{
    animation::{
        animated_field, animation_curves::AnimatableCurve, AnimatedBy, AnimationEvent,
        AnimationTargetId,
    },
    prelude::*,
};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_observer(on_footstep)
        .run();
}

#[derive(AnimationEvent, Clone)]
struct Footstep {
    foot: String,
}

fn on_footstep(event: On<Footstep>) {
    info!("Footstep: {}", event.foot);
}

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("walker");
    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(-2.0, 0.0, 0.0)),
                (1.0, Vec3::new(0.0, 0.3, 0.0)),
                (2.0, Vec3::new(2.0, 0.0, 0.0)),
                (3.0, Vec3::new(0.0, 0.3, 0.0)),
                (4.0, Vec3::new(-2.0, 0.0, 0.0)),
            ])
            .unwrap(),
        ),
    );

    clip.add_event(
        0.0,
        Footstep {
            foot: "left".into(),
        },
    );
    clip.add_event(
        2.0,
        Footstep {
            foot: "right".into(),
        },
    );

    let (graph, node_index) = AnimationGraph::from_clip(clips.add(clip));
    let mut player = AnimationPlayer::default();
    player.play(node_index).repeat();

    let entity = commands
        .spawn((
            Mesh3d(meshes.add(Cuboid::new(0.6, 1.0, 0.6))),
            MeshMaterial3d(materials.add(Color::srgb(0.8, 0.5, 0.2))),
            target_name,
            AnimationGraphHandle(graphs.add(graph)),
            player,
        ))
        .id();
    commands
        .entity(entity)
        .insert((target_id, AnimatedBy(entity)));
}

Listing 30-4:动画事件——在指定时间触发脚步声(examples/listing-30-04.rs)

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

控制台每隔 2 秒打印一次脚步信息,交替输出 "left" 和 "right"。

事件的作用域

add_event 触发在 AnimationPlayer 实体上。如果你需要事件触发在特定的骨骼实体上(比如让脚部骨骼触发脚步声),可以用 add_event_to_target(target_id, time, event),它会在匹配 AnimationTargetId 的实体上触发事件。

事件的时机

动画事件在动画系统的 PostUpdate 阶段触发。如果动画被暂停,事件不会触发。如果动画循环播放,每轮都会在相同时间点触发事件。如果动画被 seek 跳过,seek_to 方法会确保跳过的事件在下一帧被触发,而 set_seek_time 则不会。