Version:

第25章 介绍动画

第25章 介绍动画

EMotionFX 简介

在我们的游戏开发阶段,鸡卡在了空闲动画中。我们要进行的下一个改进是在鸡移动时播放行走动画,在鸡不移动时播放空闲动画。

O3DE 附带一个动画 Gem EMotionFX。在本章中,我将向您展示如何使用 EMotionFX 构建状态机,以混合空闲和行走动画。

注意:
本章的关卡和资源可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch25_animation

Simple Motion Component

如果不需要混合动画或其他复杂的动画交互,可以使用可以播放一个动画的 Simple Motion 组件。

例 25.1.Simple Motion 组件

此组件具有各种配置选项,但我们将将其从实体中删除Chicken_Actor并将其替换为 Anim Graph 组件。

注意:
Simple Motion (简单运动) 和 Animation Graph (动画图形) 组件都需要 Actor (角色) 组件。Chicken_Actor实体已经从 C:\O3DE\21.11.2\Gems\NvCloth\Assets\Objects\cloth\Chicken\Actor\chicken.fbx 分配了一个实体。角色组件为动画提供角色网格和骨骼信息。

Anim Graph Component

Animation Graph 组件将允许我们将动画图形分配给 actor,并允许我们使用我们选择的参数来控制动画。

首次添加此组件时,其字段为空。首先,我们必须创建一个 Anim graph 资产。通过主菜单 Tools → Animation Editor,或者通过单击组件上 Anim graph 属性旁边的最右侧图标打开 Animation Editor。

构建动画图

提示:
Animation Editor 应该打开一个空工作区。如果没有,请使用 File → New Workspace 重置它。

动画图是定义动画及其交互的逻辑和行为的各种节点的集合,例如将多个动画混合在一起。在高层次上,我们将构建一个状态机,它混合了 chickenidle.fbx 的空闲运动和 chickenwalking.fbx 的行走运动。这两个文件都附带了 O3DE 安装中的 NvCloth gem,位于 C:\O3DE \21.11.2\Gems\NvCloth\Assets\Objects\cloth\Chicken\Motions。

指定预览角色

为了在处理动画图形时提供视觉指导,请将角色加载到 Animation Editor 中。

  1. 转到 Actor Manager 选项卡。(如果它未打开,请使用主菜单:View → Actor Manager。
  2. 单击文件夹图标。
  3. 选择 Load actor

  1. 从 NvCloth Gem 的位置中选择 chicken.actor。这会将鸡 actor 加载到 OpenGL Render Window 中。它将作为动画图形的试验场。

测试动作

在我们花时间构建图形之前,请测试该运动是否适用于鸡。

  1. 转到“运动”选项卡,查看→运动。
  2. 点击加号“+”图标。
  3. Pick Emotion FX Motion 对话框将打开,选择 chickenidle.motion。

  1. 在动作列表中,双击 chickenidle 以测试动作。

  2. 重复 chickenwalking.motion 的步骤。

  3. 这将在 OpenGL Render Window 中播放动画,以便您查看动画的外观以及它们是否与角色匹配。

创建运动集

在开始构建动画图形之前,我们需要创建一个运动集来保存我们将要使用的运动的集合。

  1. 转到 Motion Sets 选项卡,如果未打开,请使用 View → Motions Sets。
  2. 点击加号“+”图标,创建新的动作集 MotionSet0。
  3. 在“运动集”部分,有文件夹图标,单击它并选择 chickenidle.motion

提示:
您可以双击 chickenidle 动作,在渲染窗口中对其进行测试
  1. 重复 chickenwalking.motion 的步骤。
  2. 使用软盘图标保存动作集。例如,我将其保存到 C:\git\book\Gems\MyGem\Assets\Animation\chicken.motionset。

Anim Graph

我们已准备好开始构建一个动画图形,该图形将混合行走和空闲运动。

  1. 转到 Anim Graph 选项卡,如果未打开,请使用 View → Anim Graph。

  1. 单击加号“+”图标以创建新的空 Animgraph。
  2. 在 Anim Graph 的空白工作空间中,右键单击并选择 Create Node → Sources → BlendTree。
提示:
您还可以从 Anim Graph Palette 的 Sources 组中拖放 BlendTree 节点。

  1. 双击 BlendTree0 节点以查看其配置。默认情况下,混合树只有一个 final 节点,没有其他节点。此节点将接收两个运动的混合。
提示:
您可以在 Attributes (属性) 面板中重命名节点名称。

  1. 右键单击 BlendTwo,将 Blend Two 节点添加到 FinalNode0 的左侧→→ BlendTwo → Blending 。此节点会将两个运动混合在一起,并将结果分配到 FinalNode0 中。

  1. 在 BlendTwo0 的左侧,创建两个 Motion 节点,分别→ Sources → Motion。
  2. 选择 Motion0 节点。
  3. 转到“属性”面板。
  4. 在 Motions 属性的右侧,单击加号“+”按钮。
  5. Motion Selection 窗口打开,从 MotionSet0 中选择 chickenidle motion。
  6. 对于 Motion1 节点,以相同的方式选择 chickenwalking motion。

  1. 在 Motion 节点上,有 Output Pose 输出。从 Output Pose to Pose 1 input on BlendTwo0 (BlendTwo0) 节点上的 Output Pose to Pose 1 input (输出姿势到姿势 1) 输入旁边的方块中拖动一条线。

  1. 将 BlendTwo0 节点的 Outpost 姿势与 FinalNode0 的 Input Pose 连接起来。动画图形应开始播放。如果没有,Anim Graph 的工具栏中有一个白色箭头播放按钮。

动画混合

图 25.1.两个动画的第一次混合

让我们来看看发生了什么。我们创建了一个 blend 节点。在节点内部,我们声明了两个运动源,这两个源与 Blend Two 节点混合在一起,该节点将结果输出到最终节点。目前,唯一实际播放的动作是空闲动作。这是因为 BlendTwo0 节点的 Weight input(权重输入)决定了两个运动之间的混合量。

由于我们在空闲和行走动作之间进行混合,因此根据鸡的速度进行混合是有意义的。如果速度为零,则应播放 idle motion。当小鸡开始移动时,动画图形应在两个运动之间混合,直到仅播放行走运动。

参数

为了控制混合,我们将添加一个可以从动画图形外部指定的参数。

有一个 C++ API 可以从组件执行此作,但在开始之前,我们将创建一个参数并在 Animation Editor 中对其进行测试。

  1. 转到 Parameters 窗口,使用 View → Parameter Window(如果它未打开)。
  2. 点击加号“+”图标。
  3. 选择 Add parameter(添加参数)。
  4. 对于 Name(名称),输入 Speed(速度)。
  5. 对于“最大值”,输入 6 (6.0),因为在第 18 章“角色移动”中,鸡的最大速度设置为 6。
  6. 单击“创建”以创建参数。

速度参数 7. 在 Anim graph space(动画图形空间)中,选择 Create Node → Sources → Parameters(创建节点参数)。 8. 选择 Parameters0 节点后,转到“属性”面板,然后单击“选择参数”。 9. 选择 Speed 参数。

注意:
我们的速度参数范围设置为 0 到 6 之间。但 BlendTwo0 的 Weight input (权重) 输入值的范围是从 0 到 1。我们必须使用 Range Remapper 节点重新映射范围以匹配 Weight 要求。
  1. Create Node → Math → Smoothing.

提示:
Smoothing 节点是可选的,但它允许我们平滑地提高参数的值,从而随着时间的推移在运动之间混合,而无需在 C++ 组件中执行此工作。但是,如果您需要精确控制动画输出,则必须将 Speed 参数驱动 自我。
11. Create Node → Math → Range Remapper. 12. 选择 RangeRemapper0 节点后,转到“属性”面板,将“输入最大值”更改为六 (6) 以匹配“速度”参数的范围。 13. 将 Parameters0 连接到 Smoothing0,将 Smoothing0 连接到 RangeRemapper0,将 RangeRemapper0 连接到 BlendTwo0 的权重输入。

  1. 现在,您可以通过更改 Speed 参数值来测试动画图形。

  1. 将动画图形另存为 C:\git\book\Gems\MyGem\Assets\Animation\chicken.animgraph。
  2. 将工作区另存为 C:\git\book\Gems\MyGem\Assets\Animation\chicken.emfxworkspace。

运动调整

我们有一个小鸡的行走动画,但对于我们的游戏来说太慢了。我们可以解决这个问题,加快行走动画的播放速度。

  1. 转到分配了 chickenwalking.motion 的 Motion1 节点。
  2. 在“属性”面板中,将“播放速度”更改为三 (3)。
  3. 通过修改 Speed 参数来测试动画的外观。

  1. 保存动画图形。

指定 Anim Graph

此时,我们已经完成了 Animation Editor,回到 Editor,我们可以在 Anim Graph 组件中分配新的图形。

  1. 将 Anim graph 设置为 chicken.animgraph。
  2. 将 Motion set asset 设置为 chicken.motionset。
  3. 设置动作集后,“动作动作集”属性将自行更新为 MotinoSet0。

注意:
Anim Graph (动画图形) 组件读取我们分配的动画图形,找到 Speed (速度) 参数并将其反映到 Editor 中。您可以通过在组件上手动输入值并使用 CTRL+G 播放关卡来再次测试动画

驱动速度参数

剩下的唯一工作是根据鸡的速度设置 Speed 参数。这可以通过计算鸡的速度,然后调用 SetNamedParameterFloat 来完成。

using namespace EMotionFX::Integration;
AnimGraphComponentRequestBus::Event(m_actor,
&AnimGraphComponentRequests::SetNamedParameterFloat,
"Speed", s);

为了将关注点与我们目前编写的其他组件区分开来,我创建了一个新组件 ChickenAnimationComponent,它通过 ChickenNotificationBus 从 ChickenControllerComponent 接收速度通知。

void ChickenControllerComponent::UpdateVelocity(
 const ChickenInput& input)
    {
 ...
        ChickenNotificationBus::Event(GetEntityId(),
            &ChickenNotificationBus::Events::OnChickenSpeedChanged,
            m_velocity.GetLength());
    }

OnChickenSpeedChanged 将分配 Speed 参数并完成本章的工作。

void ChickenAnimationComponent::OnChickenSpeedChanged(float s)
    {
 using namespace EMotionFX::Integration;
        AnimGraphComponentRequestBus::Event(m_actor,
            &AnimGraphComponentRequests::SetNamedParameterFloat,
 "Speed", s);
    }

例 25.2.小鸡在舞会上奔跑

源代码

注意:
本章的关卡和资源可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch25_animation

例 25.3. ChickenBus.h

 #pragma once
 #include <AzCore/Component/ComponentBus.h>
 namespace MyGem
 {
 class ChickenNotifications
        : 
public AZ::ComponentBus
    {
 public:
 virtual void OnChickenSpeedChanged(float speed) = 0;
    };
 using ChickenNotificationBus = AZ::EBus<ChickenNotifications>;
 }

例 25.4. ChickenAnimationComponent.h

#pragma once
 #include <AzCore/Component/Component.h>
 #include <MyGem/ChickenBus.h>
 namespace MyGem
 {
 class ChickenAnimationComponent
        : 
public AZ::Component
        , 
public ChickenNotificationBus::Handler
    {
 public:
        AZ_COMPONENT(ChickenAnimationComponent,
 "{ED8B6A79-AA47-44B2-91B6-64A78439B037}");
 static void Reflect(AZ::ReflectContext* rc);
 // AZ::Component interface implementation
 void Activate() override;
 void Deactivate() override;
 // ChickenNotificationBus interface
 void OnChickenSpeedChanged(float s) override;
 private:
        AZ::EntityId m_actor;
    };
 } // namespace MyGem

例 25.5. ChickenAnimationComponent.h

#include <ChickenAnimationComponent.h>
 #include <AzCore/Serialization/EditContext.h>
 #include <Integration/AnimGraphComponentBus.h>
 namespace MyGem
 {
 void ChickenAnimationComponent::Reflect(AZ::ReflectContext* rc)
    {
 if (auto sc = azrtti_cast<AZ::SerializeContext*>(rc))
        {
            sc->Class<ChickenAnimationComponent, AZ::Component>()
              ->Field("Actor", &ChickenAnimationComponent::m_actor)
              ->Version(1);
 if (AZ::EditContext* ec = sc->GetEditContext())
            {
 using namespace AZ::Edit;
                ec->Class<ChickenAnimationComponent>(
 "Chicken Animation",
 "[Player controlled chicken]")
                    ->ClassElement(ClassElements::EditorData, "")
                    ->Attribute(
                        Attributes::AppearsInAddComponentMenu,
                            AZ_CRC_CE("Game"))
                    ->DataElement(nullptr,
                        &ChickenAnimationComponent::m_actor,
 "Actor", "Entity with chicken actor.");
            }
        }
    }
 void ChickenAnimationComponent::Activate()
    {
        ChickenNotificationBus::Handler::BusConnect(GetEntityId());
    }
 void ChickenAnimationComponent::Deactivate()
    {
        ChickenNotificationBus::Handler::BusDisconnect();
    }
 void ChickenAnimationComponent::OnChickenSpeedChanged(float s)
    {
 using namespace EMotionFX::Integration;
        AnimGraphComponentRequestBus::Event(m_actor,
            &AnimGraphComponentRequests::SetNamedParameterFloat,
 "Speed", s);
    }
 } // namespace MyGem