多角色动画同步
多角色动画同步
在制作玩法时,常常会有两个角色之间有互动,同时播放动画表演的需求。比如处决,角色交互等。这其中关键点事需要预先设置好两个角色的相对位置,同时播放两个角色动画。
一般来说有两种做法:
- 参考骨骼,也就是在做主角色动画时,使用一根空骨骼作为副角色的位置。
- 插件:Contextual Animation
值得注意的是这两种方式最终都会使用Motion Warping来讲角色拉去到目标的位置和方向。
下面有两个演示:


参考骨骼
对齐两个或者多个角色,一起播放动画,处决交互等

- 使用一个
Reference Joint
位置对准两个角色 - 动画长度一般需要一样长
- 需要处理Blending问题
- 需要处理如何Align主从
- 需要动画师约束好规则,动画师在制作时更加的直观
Contextual Animation
使用
- 创建角色数据表
- 选择
Contextual Anim Roles Asset
,该资源用于区别播放动画的代称 - 配置如下,这里设置为角色,可以在动画编辑器下查看到胶囊体的大小。值得注意的是
Mesh to Component
中的位置的Z轴是默认添加了90。 该地方的数值直接从角色的蓝图中蒙皮组件中获取。 - 创建同步动画蒙太奇,值得注意动画切片都需要打开Root Motion。
- Paired_ForceChoke_Att_Montage
- Paired_ForceChoke_Vic_Montage
- 创建
Contextual Animation
动画配置 - 设置角色数据表和基础角色,基础角色可以理解为发起者。
- 创建动画序列,设置蒙太奇,这里可以设置运动方式,如果动画中有Z轴的位移(root位移),可以设置为Flying。
- 设置副动画角色的相对位置,选择时间轴中的副动画,设置
Mesh to Scene
中的数值。设置完后需要点击一下Reset Scene
按钮,重置场景。这里可以不需要设置Z轴的偏移,如果两个动画是在同一平面上播放的。 - 设置吸附点,这里配置的
Warp Target Name
是需要和在蒙太奇中的MotionWarping中的Target Name相对应。这里使用的自定义模式,其中Origin
点是设置为吸附到哪个角色点上。这里吸附上的点是包含了上一步设置的相对位置。 - 设置蒙太奇中的Motion Warping,这里需要设置Target Name和上一步的Warp Target Name相对应。其他的设置可以查看Motion Warping的说明
- 在角色蓝图中添加
ContextualAnimSceneActor
和Motion Warping
组件 - 设置播放条件,选择需要设置的动画时间轴,也就是说可以配置两种,发起者和配合者都可以配置。这里也可以实时的预览配置的效果。如果需要自定义条件,实现
UContextualAnimSelectionCriterion
类即可。 - 设置角色的碰撞
- 播放同步动画
- 创建绑定信息,设置发起者和副角色以及需要播放的CAS动画
- 播放动画
- 绑定回调函数:
- OnJointedScene
- OnLeftScene
- 创建绑定信息,设置发起者和副角色以及需要播放的CAS动画
原理
该插件代码分为两部分:
- ContextualAnimation:运行时逻辑
- ContextualAnimationEditor:编辑器
这里主要介绍运行时逻辑,也就是调用了FContextualAnimSceneBindings::TryCreateBindings
和UContextualAnimSceneActorComponent::StartContextualAnimScene
函数后的逻辑。
TryCreateBindings
在创建绑定信息时,会调用FContextualAnimSceneBindings::TryCreateBindings
函数,该函数尝试在给定场景中为一组角色创建绑定关系,使他们能够参与上下文动画互动。如果绑定成功,返回一个有效的FContextualAnimSceneBindings
对象。
在Contextual Animation Asset
中可以配置多个动画序列,通过配置的UContextualAnimSelectionCriterion
来筛选对应的动画序列。实现函数为 FContextualAnimSceneBindings::FindAnimSet(const UContextualAnimSceneAsset& SceneAsset, int32 SectionIdx, const TMap<FName, FContextualAnimSceneBindingContext>& Params)
如果配置多个实现如下:
else if (NumSets > 1)
{
const FContextualAnimSceneSection* Section = SceneAsset.GetSection(SectionIdx);
TArray<TTuple<int32, float>, TInlineAllocator<5>> ValidSets; // 0: AnimSetIdx, 1: AnimSetRandomWeight
float TotalWeight = 0;
for (int32 AnimSetIdx = 0; AnimSetIdx < NumSets; AnimSetIdx++)
{
if (CheckConditions(SceneAsset, SectionIdx, AnimSetIdx, Params))
{
const float AnimSetRandomWeight = FMath::Max(Section->GetAnimSet(AnimSetIdx)->RandomWeight, 0);
ValidSets.Add(MakeTuple(AnimSetIdx, AnimSetRandomWeight));
TotalWeight += AnimSetRandomWeight;
}
}
// Shuffle the list of valid sets to add an extra layer of randomness.
Algo::RandomShuffle(ValidSets);
float RandomValue = FMath::RandRange(0.f, TotalWeight);
for (int32 Idx = 0; Idx < ValidSets.Num(); Idx++)
{
RandomValue -= FMath::Max(ValidSets[Idx].Get<1>(), 0);
if (RandomValue <= 0)
{
AnimSetIdxSelected = ValidSets[Idx].Get<0>();
break;
}
}
}
最后找到合适的动画序列后,返回绑定信息。
StartContextualAnimScene
主要步骤:
- 计算吸附点:CalculateWarpPointsForBindings,其中核心逻辑在
FContextualAnimSceneBindings::CalculateWarpPoint
函数中,核心逻辑:
if (WarpPointDef.Mode == EContextualAnimWarpPointDefinitionMode::Custom)
{
const FContextualAnimWarpPointCustomParams& Params = WarpPointDef.Params;
if (const FContextualAnimSceneBinding* Binding = FindBindingByRole(Params.Origin))
{
OutWarpPoint.Name = WarpPointDef.WarpTargetName;
if (Params.bAlongClosestDistance)
{
if (const FContextualAnimSceneBinding* OtherBinding = FindBindingByRole(Params.OtherRole))
{
const FTransform T1 = Binding->GetTransform();
const FTransform T2 = OtherBinding->GetTransform();
OutWarpPoint.Transform.SetLocation(FMath::Lerp<FVector>(T1.GetLocation(), T2.GetLocation(), Params.Weight));
OutWarpPoint.Transform.SetRotation((T2.GetLocation() - T1.GetLocation()).GetSafeNormal2D().ToOrientationQuat());
return true;
}
}
else
{
OutWarpPoint.Transform = Binding->GetTransform();
return true;
}
}
}
- 发起者,配合者加入场景:JoinScene,
UContextualAnimSceneActorComponent::JoinScene
,主要实现
if (Bindings.IsValid())
{
LeaveScene();
}
if (const FContextualAnimSceneBinding* Binding = InBindings.FindBindingByActor(GetOwner()))
{
UE_LOG(LogContextualAnim, Verbose, TEXT("%-21s UContextualAnimSceneActorComponent::JoinScene Actor: %s Role: %s InBindings Id: %d Section: %d Asset: %s"),
*UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetOwner()->GetLocalRole()), *GetNameSafe(GetOwner()), *InBindings.GetRoleFromBinding(*Binding).ToString(), InBindings.GetID(), InBindings.GetSectionIdx(), *GetNameSafe(InBindings.GetSceneAsset()));
AnimsPlayed.Reset();
Bindings = InBindings;
const FContextualAnimTrack& AnimTrack = Bindings.GetAnimTrackFromBinding(*Binding);
PlayAnimation_Internal(AnimTrack.Animation, 0.f, Bindings.ShouldSyncAnimation());
AddOrUpdateWarpTargets(AnimTrack.SectionIdx, AnimTrack.AnimSetIdx, WarpPoints, ExternalWarpTargets);
SetCollisionState(*Binding);
SetMovementState(*Binding, AnimTrack);
OnJoinScene(*Binding);
OnJoinedSceneDelegate.Broadcast(this);
}