动画基础

概述

学习目标

  • 理解 Bevy 动画系统的基本概念
  • 掌握动画网格的创建和播放
  • 了解动画变换的使用
  • 学会使用动画播放器和动画图

前置知识要求

  • Bevy 快速入门
  • ECS 基础
  • 3D 开发基础
  • 资源管理基础

核心概念

什么是动画系统?

动画系统是 Bevy 中用于创建和播放动画的功能。Bevy 的动画系统支持网格动画、变换动画、UI 动画等多种类型的动画。

为什么需要动画系统?

  1. 视觉效果:动画可以增强游戏的视觉效果
  2. 用户体验:动画可以改善用户体验
  3. 游戏性:动画是游戏性的重要组成部分
  4. 艺术表现:动画可以增强艺术表现力

动画系统的核心组件

Bevy 动画系统包含以下核心组件:

  • AnimationClip:动画片段,包含动画数据
  • AnimationGraph:动画图,用于组织和管理动画
  • AnimationPlayer:动画播放器,用于播放动画
  • AnimationTarget:动画目标,用于指定动画作用的对象

基础用法

动画网格

创建和播放动画网格。

源代码文件bevy/examples/animation/animated_mesh.rs

代码示例

use bevy::{light::CascadeShadowConfigBuilder, prelude::*, scene::SceneInstanceReady};

// 包含网格和动画的示例资源
const GLTF_PATH: &str = "models/animated/Fox.glb";

fn main() {
    App::new()
        .insert_resource(AmbientLight {
            color: Color::WHITE,
            brightness: 2000.,
            ..default()
        })
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup_mesh_and_animation)
        .add_systems(Startup, setup_camera_and_environment)
        .run();
}

// 一个存储我们要播放的动画引用的组件。这在我们开始加载网格时创建
// (参见 `setup_mesh_and_animation`),并在网格生成时读取(参见 `play_animation_when_ready`)。
#[derive(Component)]
struct AnimationToPlay {
    graph_handle: Handle<AnimationGraph>,
    index: AnimationNodeIndex,
}

fn setup_mesh_and_animation(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut graphs: ResMut<Assets<AnimationGraph>>,
) {
    // 创建一个包含单个动画的动画图。我们想要示例资源中的"run"动画,其索引为二。
    let (graph, index) = AnimationGraph::from_clip(
        asset_server.load(GltfAssetLabel::Animation(2).from_asset(GLTF_PATH)),
    );

    // 将动画图存储为资源。
    let graph_handle = graphs.add(graph);

    // 创建一个存储我们动画引用的组件。
    let animation_to_play = AnimationToPlay {
        graph_handle,
        index,
    };

    // 开始将资源作为场景加载,并将其引用存储在 SceneRoot 组件中。
    // 此组件将在资源加载后自动生成包含我们网格的场景。
    let mesh_scene = SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(GLTF_PATH)));

    // 生成一个包含我们组件的实体,并将其连接到一个观察者,
    // 该观察者将在场景加载并生成时触发。
    commands
        .spawn((animation_to_play, mesh_scene))
        .observe(play_animation_when_ready);
}

fn play_animation_when_ready(
    scene_ready: On<SceneInstanceReady>,
    mut commands: Commands,
    children: Query<&Children>,
    animations_to_play: Query<&AnimationToPlay>,
    mut players: Query<&mut AnimationPlayer>,
) {
    // 我们在 `setup_mesh_and_animation` 中生成的实体是触发器的目标。
    // 首先找到我们添加到该实体的 AnimationToPlay 组件。
    if let Ok(animation_to_play) = animations_to_play.get(scene_ready.entity) {
        // SceneRoot 组件将场景生成为我们实体的子实体的层次结构。
        // 由于资源包含蒙皮网格和动画,它还会生成一个动画播放器组件。
        // 搜索我们实体的后代以找到动画播放器。
        for child in children.iter_descendants(scene_ready.entity) {
            if let Ok(mut player) = players.get_mut(child) {
                // 告诉动画播放器开始动画并保持重复播放。
                //
                // 如果你想尝试停止和切换动画,请参阅 `animated_mesh_control.rs` 示例。
                player.play(animation_to_play.index).repeat();

                // 添加动画图。这只需要执行一次即可将动画播放器连接到网格。
                commands
                    .entity(child)
                    .insert(AnimationGraphHandle(animation_to_play.graph_handle.clone()));
            }
        }
    }
}

关键要点

  • 使用 AnimationGraph::from_clip 从动画片段创建动画图
  • 使用 AnimationPlayer 播放动画
  • 使用 AnimationGraphHandle 将动画图连接到实体
  • 使用 SceneInstanceReady 事件在场景加载后播放动画

说明: 动画网格是动画系统的基础。通过加载包含动画的 glTF 文件,可以创建和播放动画网格。

动画变换

创建和播放动画变换。

源代码文件bevy/examples/animation/animated_transform.rs

代码示例

use bevy::{
    animation::{animated_field, AnimationTarget, AnimationTargetId},
    prelude::*,
};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(AmbientLight {
            color: Color::WHITE,
            brightness: 150.0,
            ..default()
        })
        .add_systems(Startup, setup)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut animations: ResMut<Assets<AnimationClip>>,
    mut graphs: ResMut<Assets<AnimationGraph>>,
) {
    // 相机
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    // 光源
    commands.spawn((
        PointLight {
            intensity: 500_000.0,
            ..default()
        },
        Transform::from_xyz(0.0, 2.5, 0.0),
    ));

    // 让我们使用 `Name` 组件来定位实体。我们可以使用任何我们喜欢的东西,
    // 但名称很方便。
    let planet = Name::new("planet");
    let orbit_controller = Name::new("orbit_controller");
    let satellite = Name::new("satellite");

    // 创建动画
    let mut animation = AnimationClip::default();
    // 曲线可以修改变换的单个部分:这里,平移。
    let planet_animation_target_id = AnimationTargetId::from_name(&planet);
    animation.add_curve_to_target(
        planet_animation_target_id,
        AnimatableCurve::new(
            animated_field!(Transform::translation),
            UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([
                Vec3::new(1.0, 0.0, 1.0),
                Vec3::new(-1.0, 0.0, 1.0),
                Vec3::new(-1.0, 0.0, -1.0),
                Vec3::new(1.0, 0.0, -1.0),
                // 如果需要无缝循环,最后一帧应该与第一帧相同
                Vec3::new(1.0, 0.0, 1.0),
            ]))
            .expect("应该能够构建平移曲线,因为我们传入有效的样本"),
        ),
    );
    // 或者它可以修改变换的旋转。
    // 为了找到要修改的实体,将遍历层次结构,在每个级别查找具有正确名称的实体。
    let orbit_controller_animation_target_id =
        AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter());
    animation.add_curve_to_target(
        orbit_controller_animation_target_id,
        AnimatableCurve::new(
            animated_field!(Transform::rotation),
            UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([
                Quat::IDENTITY,
                Quat::from_axis_angle(Vec3::Y, PI / 2.),
                Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
                Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
                Quat::IDENTITY,
            ]))
            .expect("无法构建旋转曲线"),
        ),
    );

    // 创建动画图
    let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation));

    // 创建动画播放器,并设置为重复播放
    let mut player = AnimationPlayer::default();
    player.play(animation_index).repeat();

    // 创建将被动画化的场景
    // 第一个实体是行星
    let planet_entity = commands
        .spawn((
            Mesh3d(meshes.add(Sphere::default())),
            MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
            // 添加动画图和播放器
            planet,
            AnimationGraphHandle(graphs.add(graph)),
            player,
        ))
        .id();
    commands.entity(planet_entity).insert((
        AnimationTarget {
            id: planet_animation_target_id,
            player: planet_entity,
        },
        children![(
            Transform::default(),
            Visibility::default(),
            orbit_controller,
            AnimationTarget {
                id: orbit_controller_animation_target_id,
                player: planet_entity,
            },
            children![(
                Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))),
                MeshMaterial3d(materials.add(Color::srgb(0.3, 0.9, 0.3))),
                Transform::from_xyz(1.5, 0.0, 0.0),
                AnimationTarget {
                    id: satellite_animation_target_id,
                    player: planet_entity,
                },
                satellite,
            )],
        )],
    ));
}

关键要点

  • 使用 AnimationClip 创建动画片段
  • 使用 AnimatableCurve 创建动画曲线
  • 使用 animated_field! 宏指定动画字段
  • 使用 AnimationTarget 指定动画目标

说明: 动画变换是动画系统的重要功能。通过创建动画曲线,可以动画化实体的变换属性。

进阶用法

动画播放器

使用动画播放器控制动画播放。

关键信息

  • 使用 AnimationPlayer::play() 播放动画
  • 使用 .repeat() 重复播放动画
  • 使用 .once() 播放一次动画
  • 使用 .pause() 暂停动画
  • 使用 .resume() 恢复动画

说明: 动画播放器是动画系统的核心组件。通过使用动画播放器,可以控制动画的播放状态。

实际应用

在游戏开发中的应用场景

动画系统在游戏开发中有广泛的应用:

  1. 角色动画:创建角色行走、跑步、跳跃等动画
  2. 物体动画:创建物体移动、旋转、缩放等动画
  3. UI 动画:创建 UI 元素的动画效果
  4. 特效动画:创建特效的动画效果

常见问题

问题 1:如何加载动画网格?

解决方案

  • 使用 AssetServer 加载 glTF 文件
  • 使用 GltfAssetLabel::Animation 加载动画
  • 使用 GltfAssetLabel::Scene 加载场景
  • 使用 SceneInstanceReady 事件在场景加载后播放动画

问题 2:如何创建动画变换?

解决方案

  • 使用 AnimationClip 创建动画片段
  • 使用 AnimatableCurve 创建动画曲线
  • 使用 animated_field! 宏指定动画字段
  • 使用 AnimationTarget 指定动画目标

问题 3:如何控制动画播放?

解决方案

  • 使用 AnimationPlayer::play() 播放动画
  • 使用 .repeat() 重复播放动画
  • 使用 .pause() 暂停动画
  • 使用 .resume() 恢复动画

性能考虑

  1. 动画图:使用动画图组织和管理动画
  2. 动画目标:使用动画目标指定动画作用的对象
  3. 动画播放器:合理使用动画播放器控制动画播放

相关资源

相关源代码文件

  • bevy/examples/animation/animated_mesh.rs - 动画网格示例
  • bevy/examples/animation/animated_transform.rs - 动画变换示例

官方文档链接

进一步学习建议

  • 学习动画进阶,了解动画图、动画事件等高级功能
  • 学习 UI 动画,了解 UI 动画的创建和使用

索引返回上级目录