重点剖析的类:
参考:https://zhuanlan.zhihu.com/p/405437842
参考:https://blog.csdn.net/qq_23030843/article/details/109103433
参考:https://ikrima.dev/ue4guide/gameplay-programming/animation-subsystem/animation-subsystem/
参考:https://zhuanlan.zhihu.com/p/393884450
参考:https://arrowinmyknee.com/2019/09/11/a-deep-look-into-animation-framework-in-ue4/
参考:https://zhuanlan.zhihu.com/p/499277229
动画系统的最主要的四个类:
UAnimInstance是动画蓝图的父类,通常我们会继承UAnimInstance生成自己的C++动画类或蓝图类,由该类创建驱动动画状态机和各种动画节点的变量,通过这些值控制动画状态机流转,控制动画权重等。
USkeletalMeshComponent是一个组件,用来创建USkeletalMesh的实例,里面会存UAnimInstance的引用,可以播放动画。但在该组件里是无法修改Bone的Transform的,这是因为UE想把GamePlay和Animation系统解耦,USkeletalMeshComponent负责GamePlay,而AnimInstance负责动画。所以SkeletalMeshComponent和AnimInstance是包含关系:
UAnimInstance* USkeletalMeshComponent::GetAnimInstance() const
{
return AnimScriptInstance;
}
FAnimInstanceProxy是UAnimInstace的代理,保存大量动画相关数据,例如AnimInstance,SkeletalMeshComponent,ComponentTransform等,同时分担动画蓝图的更新工作,可以被多线程访问。大部分动画图形访问的数据已经从UAnimInstance 移至一个新的结构,名为FAnimInstanceProxy 。 该代理结构存放有关UAnimInstance
的大量数据。
FAnimNode_Base是所有动画节点的基类,例如TwoBoneIk、TransfromBone等,根据需要执行不同的动画计算任务。
UE跟Unity一样,是EC架构,它的动画系统是通过SkeletalMeshComponent来驱动的,主要分为UpdateAnimation和ParallelAnimationEvaluation两个阶段:
不过也可以修改命令行参数a.ParallelAnimEvaluation
,让第二阶段在Game线程里执行,如下图所示,开启多线程动画更新,应该就会让动画数据的Apply过程用多线程的方式进行:
此阶段始于USkeletalMeshComponent::TickComponent
,会在里面调用USkeletalMeshComponent::TickPose
,再在SkeletalMeshComponent::TickAnimation()
函数里,针对里面个每个AnimInstance调用UAnimInstance::UpdateAnimation
函数。
整体流程是:
主要函数为:
void USkeletalMeshComponent::TickAnimation(float DeltaTime, bool bNeedsValidRootMotion)
{
SCOPED_NAMED_EVENT(USkeletalMeshComponent_TickAnimation, FColor::Yellow);
SCOPE_CYCLE_COUNTER(STAT_AnimGameThreadTime);
SCOPE_CYCLE_COUNTER(STAT_AnimTickTime);
// if curves have to be refreshed before updating animation
if (!AreRequiredCurvesUpToDate())
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_USkeletalMeshComponent_RefreshBoneTransforms_RecalcRequiredCurves);
RecalcRequiredCurves();
}
if (SkeletalMesh != nullptr)
{
// We're about to UpdateAnimation, this will potentially queue events that we'll need to dispatch.
bNeedsQueuedAnimEventsDispatched = true;
// Tick all of our anim instances
// 1. 先后调用各个Instance的UpdateAnimation函数
TickAnimInstances(DeltaTime, bNeedsValidRootMotion);
/**
If we're called directly for autonomous proxies, TickComponent is not guaranteed to get called.
So dispatch all queued events here if we're doing MontageOnly ticking.
*/
if (ShouldOnlyTickMontages(DeltaTime))
ConditionallyDispatchQueuedAnimEvents();
}
}
CachedAnimCurveUidVersion
更新动画曲线列表时,会通过CachedAnimCurveUidVersion来判断动画曲线是否已经更新过,CachedAnimCurveUidVersion是一个缓存的uint8,用来标记当前的动画曲线id版本,动画曲线更新后就会刷新该值,通过与该值进行比对就可以判断本次更新是否已经完成任务,防止多次操作。
前面说的LinkedAnimInstan,AnimScriptInstance、PostProcessAnimInstance都是UAnimInstance类的不同实例,存在USkeletalMeshComponent
里,代码如下所示:
class ENGINE_API USkeletalMeshComponent : public USkinnedMeshComponent, public IInterface_CollisionDataProvider
{
...
private:
/** Any running linked anim instances */
UPROPERTY(transient)
TArray<TObjectPtr<UAnimInstance>> LinkedInstances;// 这是个数组
/** The active animation graph program instance. */
UPROPERTY(transient, NonTransactional)
TObjectPtr<UAnimInstance> AnimScriptInstance;
/** An instance created from the PostPhysicsBlueprint property of the skeletal mesh we're using,
* Runs after (and receives pose from) the main anim instance.
*/
UPROPERTY(transient)
TObjectPtr<UAnimInstance> PostProcessAnimInstance;
}
LinkedAnimInstance是我们通过LinkAnimGraph动画节点链接上去的动画蓝图,可以作为独立的动画功能模块,根据需要使用,可动态插拔,在实际使用中更具灵活性,如下图所示:
而AnimScriptInstance其实就是常规的动画蓝图,每个SkeletalMeshComponent都只有一个动画蓝图,所以代码里只存了一个,正常的动画,比如Locomotion、动画分层、混合、Montage插槽都是在这个Instance里完成的,会装载在SkeletalMesh组件的插槽上:
而PostProcessAnimInstance也是作为一个特殊的Instance,感觉是个后处理的Instance,一般用于IK、物理模拟动画节点、表情动画及其他的动画节点计算任务,在PostProcess-AnimInstance中,我们首先接收来自AnimScriptInstance动画蓝图中的pose,在此基础上进行计算出新的OutputPose:
UAnimInstance::UpdateAnimation
里会更新这三种Instance,如下所示:
// 按顺序更新三种Instance的UpdateAnimation函数
void USkeletalMeshComponent::TickAnimInstances(float DeltaTime, bool bNeedsValidRootMotion)
{
// We update linked instances first incase we're using either root motion or non-threaded update.
// This ensures that we go through the pre update process and initialize the proxies correctly.
for (UAnimInstance* LinkedInstance : LinkedInstances)
{
// Sub anim instances are always forced to do a parallel update
LinkedInstance->UpdateAnimation(DeltaTime * GlobalAnimRateScale, false, UAnimInstance::EUpdateAnimationFlag::ForceParallelUpdate);
}
if (AnimScriptInstance != nullptr)
{
// Tick the animation
AnimScriptInstance->UpdateAnimation(DeltaTime * GlobalAnimRateScale, bNeedsValidRootMotion);
}
if(ShouldUpdatePostProcessInstance())
{
PostProcessAnimInstance->UpdateAnimation(DeltaTime * GlobalAnimRateScale, false);
}
}
每个Instance的UpdateAnimation
函数里主要做了这么些事情:
FAnimInstanceProxy::PreUpdate
函数OnPreUpdate
函数OnPostUpdate
函数ParallelUpdateAnimation
和PostUpdateAnimation
函数大概是这样:
// 为了支持多线程, 要预先处理动画
PreUpdateAnimation(DeltaSeconds);// 里面会调用AnimInstanceProxy的PreUpdate函数, 进而调用AnimGraph里每个Node的PreUpdate函数, 用于更新节点的game-play data
// 下面三个函数都是更新Montage的, 这段代码需要执行在C++的NativeUpdateAnimation之前, 这样节点才知道where montage is
UpdateMontage(DeltaSeconds);
// now we know all montage has advanced time to test sync groups
UpdateMontageSyncGroup();
// Update montage eval data, to be used by AnimGraph Update and Evaluate phases.
UpdateMontageEvaluationData();
NativeUpdateAnimation();
// 蓝图的Update Event会在此阶段执行
BlueprintUpdateAnimation();
// 直接在主线程更新动画, 相当于没有多线程
if(bShouldImmediateUpdate)
{
// cant use parallel update, so just do the work here (we call this function here to do the work on the game thread)
ParallelUpdateAnimation();
// At this point, notifies are handled.
PostUpdateAnimation();
}
参考:https://blog.csdn.net/ttm2d/article/details/106557731
参考:[UE4]C++设置AnimInstance的相关问题
参考:https://www.youtube.com/watch?v=6VMOCO-JcOQ&ab_channel=JollyMonsterStudio
AnimInstance是Animation Blueprint的C++版本,在代码里是通过Skeletal Mesh组件设置的:
USkeletalMeshComponent::SetAnimInstanceClass(UClass* NewClass);
举个例子:
// 加载AnimInstance, 对应的类叫UAnimBlueprintGeneratedClass
UAnimBlueprintGeneratedClass* MeshAnim = LoadObject<UAnimBlueprintGeneratedClass>(NULL, TEXT("/Game/Character/HeroTPP_AnimBlueprint.HeroTPP_AnimBlueprint"));
Mesh->SetAnimInstanceClass(MeshAnim);
也可以创建自己的AnimInstance
类:
//MyAnimInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "MyAnimInstance.generated.h"
UCLASS(transient, Blueprintable, hideCategories=AnimInstance, BlueprintType, meta=(BlueprintThreadSafe), Within=SkeletalMeshComponent)
class THIRDPERSONCPP_API UMyAnimInstance : public UAnimInstance
{
GENERATED_BODY()
UFUNCTION(BlueprintCallable)
UActorComponent* GetSiblingComponentByClass(TSubclassOf<UActorComponent> ComponentClass) const;
};
在UE5项目里创建新的子类,看到这里有ControlRigAnimInstance
,不过这里直接继承AnimInstance
:
默认生成的代码如下:
// MyAnimInstance.h文件
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "MyAnimInstance.generated.h"
/**
*
*/
UCLASS()
class MYPROJECT_API UMyAnimInstance : public UAnimInstance
{
GENERATED_BODY()
};
// MyAnimInstance.cpp文件
#include "MyAnimInstance.h"
把一个模型拖拽到UE5工程里,默认会为其创建SkeletalMesh、Skeleton和Physics Asset三种文件资源,然后把Skeletal Mesh拖到场景里,选择器SkeletalMeshComponent,指认其AnimInstance类:
接下来在类里创建变量:
UCLASS()
class MYPROJECT_API UMyAnimInstance : public UAnimInstance
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Anim Instance)
float Speed;
};
接下来可以实现一些初始化的函数了,UAnimInstance
类里提供了虚函数NativeInitializeAnimation
,一般会在这个函数里进行post initialized definitions
,类似于BeginPlay
函数或者Player类的PostIntialize
函数:
// the below functions are the native overrides for each phase Native initialization override point
virtual void NativeInitializeAnimation();// 相当于BeginPlay函数
// Native update override point. It is usually a good idea to simply gather data in this step and
// for the bulk of the work to be done in NativeThreadSafeUpdateAnimation.
// 相当于Tick函数, 等同于Blueprint里的EventBlueprintUpdateAnimation节点
virtual void NativeUpdateAnimation(float DeltaSeconds);
参考:https://programmer.group/ue4-c-preliminary-use-of-animation-and-behavior-tree-related-modules.html
AnimInstanceProxy属于多线程动画优化系统的核心对象。他可以分担动画蓝图的更新工作,将部分动画蓝图的任务分配到其他线程去做。此类基本包含了所有在AnimGraph里用到的数据,它存了AnimGraph的根节点指针,还有比如bones、notifies、状态机、pose snapshots等数据。
一般而言,不能从动画图形节点(Update/Evaluate calls)访问或修改UAnimInstance
,因为它们可以在其他线程上运行。 有一些锁定封装器(GetProxyOnAnyThread 和GetProxyOnGameThread )可以在任务运行期间阻止访问`FAnimInstanceProxy。
主要想法是在最差的情况下,任务等待完成,然后才允许从代理读取或写入数据。
从动画图形的角度而言,从动画节点只能访问FAnimInstanceProxy,而不能访问
UAnimInstance。 对于FAnimInstanceProxy::PreUpdate 或FAnimInstaceProxy::PreEvaluateAnimation 中的每次更新,必须与代理交换数据(通过缓冲、复制或其他策略)。 接下来需要被外部对象访问的任何数据应该从FAnimInstanceProxy::PostUpdate 中的代理进行交换/复制。
这与UAnimInstance
的一般用法冲突,在一般用法中,可以在任务运行期间从其他类访问成员变量。 建议最好不要从其他类直接访问动画实例。动画实例应从其他位置拉取数据。
总之,将游戏逻辑得更新从UAnimInstance转移到AnimInstanceProxy,并且动画图表中只能访问AnimInstanceProxy中得数据,从而做并行优化。
AnimInstanceProxy
类的主要数据有:
IAnimClassInterface*
FAnimNode_Base* RootNode;
UE会从它开始遍历整个GraphFAnimSync Sync
这里管理了我们所有的用户输入和计算过程,对应的UAnimGraphNode负责相关内容的UI,参与计算的pin还是在FAnimNode上管理的
几个主要的函数:
// Called when the node first runs. If the node is inside a state machine or
// cached pose branch then this can be called multiple times
Initialize_AnyThread(const FAnimationInitializeContext& Context);// 可能会被调用多次
// Called to cache any bones that this node needs to track (e.g. in a FBoneReference).
// This is usually called at startup when LOD switches occur.
CacheBones_AnyThread(const FAnimationCacheBonesContext& Context);// 缓存bones
InitializeBoneReferences(const FBoneContainer& RequiredBones);
IsValidToEvaluate(const USkeleton* Skeleton, const FBoneContainer& RequiredBones)
Update_AnyThread(const FAnimationUpdateContext& Context)
Initialize_AnyThread
处理初始化的函数,FAnimNode_Base
里这个函数是空的,在子类FAnimNode_AssetPlayerBase
里:
void FAnimNode_AssetPlayerBase::Initialize_AnyThread(const FAnimationInitializeContext& Context)
{
FAnimNode_Base::Initialize_AnyThread(Context);
MarkerTickRecord.Reset();
bHasBeenFullWeight = false;
}
对于非Component Space的节点,可以在这里对BasePose进行初始化,也可以对各种数值进行Initialize,相比起后面的Update,在这里初始化会便宜很多,因为这个函数只会在编译时,游戏启动时和LOD改变时被调用。
CacheBones_AnyThread
用于缓存输入姿态,对于Local Space节点而言,可以在这里对Local Space的Pose进行cache
其实每个Anim Node,UE都是分为两个部分的,一部分在Runtime,叫做FAnimNode_Base
,是Runtime的动画节点;还有一部分在Editor,叫UAnimGraphNode_Base
,只会在Editor下,作为Anim Graph里的节点。
动画节点会有以下函数:
相当于一个AnimationUtility类,里面的都是static函数
类的核心数据前面提到过,就是好几个AnimInstance:
class ENGINE_API USkeletalMeshComponent : public USkinnedMeshComponent, public IInterface_CollisionDataProvider
{
...
public:
// Component里设置的Anim Instance(The active animation graph program instance)
UPROPERTY(transient, NonTransactional)
TObjectPtr<UAnimInstance> AnimScriptInstance;
/** An instance created from the PostPhysicsBlueprint property of the skeletal mesh we're using,
* Runs after (and receives pose from) the main anim instance.
负责后处理的Instance */
UPROPERTY(transient)
TObjectPtr<UAnimInstance> PostProcessAnimInstance;
// 不知道干啥的?
/* The AnimBlueprint class to use. Use 'SetAnimInstanceClass' to change at runtime. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Animation)
class TSubclassOf<UAnimInstance> AnimClass;
private:
/** Any running linked anim instances */
UPROPERTY(transient)
TArray<TObjectPtr<UAnimInstance>> LinkedInstances;// 这是个数组
}
USkeletalMeshComponent
里的几个重要接口:
TickPose(float DeltaTime, bool bNeedsValidRootMotion)
:TickAnimation(float DeltaTime, bool bNeedsValidRootMotion);
更新的入口便是USkeletalMeshComponent的TickComponent函数,不过主要过程在父类的TickComponent
函数里,它会:
这样做是为了优化,被剔除的角色可以不进行Pose的更新或者Bone的更新,可以修改VisibilityBasedAnimTickOption这个变量来调整优化策略。调用完这个,就会调用Component里所有AnimInstance的UpdateAnimation函数了,就进入到了另外一个环节。
参考:https://docs.unrealengine.com/5.0/en-US/animation-optimization-in-unreal-engine/
这一章主要介绍一些动画的优化操作,有的参数已经默认设置好了,有的还没有。
项目设置->Genereal Settings-> Anim Blueprints下的Allow Multi Threaded AnimationUpdate选项,勾选之后,可以让更多的动画代码在work线程里进行,这一项是默认勾选的:
这里的多线程是对项目的总体设置,每个动画蓝图,也有单独的设置选项,如下图所示:
在正常情况下,也就是开启多线程动画时,AnimGraph里的节点不应该知道AnimInstance,因为AnimInstance是多个线程共享的,随时可能改变。AnimInstance里的数据实际上会被拷贝到AnimInstanceProxy
里,这是一个struct,用于处理AnimInstance和AnimNode的数据交互。UE通过GetProxyOnAnyThread
和GetProxyOnGameThread
给它加了锁,保证了线程安全。
因此,Animation节点只能获取FAnimInstanceProxy
对象,而不是AnimInstance
。每一次tick动画节点(或者是Copy动画节点),都需要通过FAnimInstanceProxy::PreUpdate
或FAnimInstaceProxy::PreEvaluateAnimation
函数,用于动画节点与proxy间的数据交换。如果外部对象需要获取动画数据,那么应该去读取在FAnimInstanceProxy::PostUpdate
里得到的数据。
Tip: 由于动画多线程的原因,最好不要直接在Anim Instance里获取成员变量的值,Instead, the Anim Instance should pull data from elsewhere
先声明一个AnimInstance:
UCLASS(Transient, Blueprintable)
class UExampleAnimInstance : public UAnimInstance
{
GENERATED_UCLASS_BODY()
private:
// The AllowPrivateAccess meta flag will allow this to be exposed to Blueprint,
// but only to graphs internal to this class.
UPROPERTY(Transient, BlueprintReadOnly, Category = "Example", meta = (AllowPrivateAccess = "true"))
FExampleAnimInstanceProxy Proxy;
// Override这俩虚函数即可
virtual FAnimInstanceProxy* CreateAnimInstanceProxy() override
{
// override this to just return the proxy on this instance
return &Proxy;
}
virtual void DestroyAnimInstanceProxy(FAnimInstanceProxy* InProxy) override
{
}
friend struct FExampleAnimInstanceProxy;
};
然后创建对应的FAnimInstanceProxy
类:
USTRUCT()
struct FExampleAnimInstanceProxy : public FAnimInstanceProxy
{
GENERATED_BODY()
FExampleAnimInstanceProxy() FAnimInstanceProxy()
{}
FExampleAnimInstanceProxy(UAnimInstance* Instance);
virtual void Update(float DeltaSeconds) override
{
// Update internal variables
MovementAngle += 1.0f * DeltaSeconds;
HorizontalSpeed = FMath::Max(0.0f, HorizontalSpeed - DeltaSeconds);
}
public:
// 把实际数据存在Proxy里, 而不是AnimInstance中
UPROPERTY(Transient, BlueprintReadWrite, EditAnywhere, Category = "Example")
float MovementAngle;
UPROPERTY(Transient, BlueprintReadWrite, EditAnywhere, Category = "Example")
float HorizontalSpeed;
};
开启之后,动画蓝图的逻辑会尽量直接访问成员变量,引擎会在编译时把蓝图里的变量复制到Native Code中,从而避免在运行时进入蓝图虚拟机(Blueprint Virtual Machine)执行蓝图代码,因为蓝图VM运行效率低。
默认会被编译优化的参数类型包括:
如下图所示,这里的闪电图标代表每个Node都是用的Fast path来读取变量:
如果改变了这些参数,让其在蓝图中执行,那么Fast Path就会失效:
感觉是如果输入的参数为常量,那么Fast Path就会生效,更多的例子参考:https://docs.unrealengine.com/5.0/en-US/animation-optimization-in-unreal-engine/
为了保证动画蓝图有使用Fast Path,可以开启Warn About Blueprint Usage,在动画蓝图的类设置下面:
然后UE就会对所有没有使用Fast Path的节点发出警告了,如下图所示:
1. 保证Parallel Updates满足条件
主要是让动画的update阶段发生在work线程上,为了满足这个条件,可以看UAnimInstance::NeedsImmediateUpdate
函数,如下所示:
// 只要这个函数返回false, 就可以多线程更新动画
bool UAnimInstance::NeedsImmediateUpdate(float DeltaSeconds) const
{
const bool bUseParallelUpdateAnimation = (GetDefault<UEngine>()->bAllowMultiThreadedAnimationUpdate && bUseMultiThreadedAnimationUpdate) || (CVarForceUseParallelAnimUpdate.GetValueOnGameThread() != 0);
return
!CanRunParallelWork() ||
GIntraFrameDebuggingGameThread ||
CVarUseParallelAnimUpdate.GetValueOnGameThread() == 0 ||
CVarUseParallelAnimationEvaluation.GetValueOnGameThread() == 0 ||
!bUseParallelUpdateAnimation ||
DeltaSeconds == 0.0f ||
RootMotionMode == ERootMotionMode::RootMotionFromEverything;
}
举个例子,如果我设置了RootMotionFromEverything
,那么角色的移动就不是多线程的,此时不可以使用Parallel Updates
2. 避免产生对蓝图虚拟机的调用
具体有以下做法:
AnimInstance
和AnimInstanceProxy
的派生类,在FAnimInstanceProxy::Update
or FAnimInstanceProxy::Evaluate
里执行相关Event Graph的逻辑,因为他们会执行于work线程总之,就是避免调用到蓝图的虚拟机里
3. Use Update Rate Optimizations (URO)
补充参考:https://zhuanlan.zhihu.com/p/60473804
可以防止动画Tick太频繁,推荐好像是往1s 15次这个频率靠。在游戏里,不同LOD的动画可以有不同的URO,如下图所示,左一是每一帧都更新;左边二是每四帧更新一次,中间用插值;第三张图是每十帧更新一次,中间用插值;最后一张图是每四帧更新一次,不用插值:
这里的插值是动画KeyFrame之间的插值,当角色离得很远的时候,其实可以disable动画插值。
具体设置,需要在Skeletal Mesh Component下勾选Enable Update Rate Optimizations
,具体的代码在AnimUpdateRateTick()
里:
void AnimUpdateRateTick(FAnimUpdateRateParametersTracker* Tracker, float DeltaTime, bool bNeedsValidRootMotion)
{
// Go through components and figure out if they've been recently rendered, and the biggest MaxDistanceFactor
bool bRecentlyRendered = false;
bool bPlayingNetworkedRootMotionMontage = false;
bool bUsingRootMotionFromEverything = true;
float MaxDistanceFactor = 0.f;
int32 MinLod = MAX_int32;
const TArray<USkinnedMeshComponent*>& SkinnedComponents = Tracker->RegisteredComponents;
for (USkinnedMeshComponent* Component : SkinnedComponents)
{
bRecentlyRendered |= Component->bRecentlyRendered;
MaxDistanceFactor = FMath::Max(MaxDistanceFactor, Component->MaxDistanceFactor);
bPlayingNetworkedRootMotionMontage |= Component->IsPlayingNetworkedRootMotionMontage();
bUsingRootMotionFromEverything &= Component->IsPlayingRootMotionFromEverything();
// 获取所有SkinnedMeshComponent的最低等级的lod, 即最精细的lod等级
MinLod = FMath::Min(MinLod, Tracker->UpdateRateParameters.bShouldUseMinLod ? Component->MinLodModel : Component->GetPredictedLODLevel());
}
bNeedsValidRootMotion &= bPlayingNetworkedRootMotionMontage;
// Figure out which update rate should be used.
AnimUpdateRateSetParams(Tracker, DeltaTime, bRecentlyRendered, MaxDistanceFactor, MinLod, bNeedsValidRootMotion, bUsingRootMotionFromEverything);
}
还有个相关Debug的设置,可以在Debug时关闭URO:
4. Enable Component Use Fixed Skel Bounds
Skeletal Mesh Component里勾选Component Use Skel Bounds ,会让角色弃用Physics Asset,只用一个Box来代替Collider,从此会略过每帧为了Culling做的recalculating bounding volumes阶段,从而提高性能
当对UE的项目进行profiling时,可能会发现,在work threads完成后,FParallelAnimationCompletionTask
函数会在Game Thread上为了Skeletal Meshes被调用。只要满足parallel updates的条件,这将会是你在profile里看到的主线程上的动画相关的主要内容,基于你的设置,在主线程里面会仍然会有下面这一部分内容:
Animation Slots可以用于帮助插入一次性动画,一般主要用于Animation Montages或Sequencer,相当于一个存放临时Pose的动画节点。