写在前面
这一节开始,我们开始着手AimOffset,一个特殊的Blend Space,用来产生角色瞄准时的头部转向和枪口瞄准效果。但在完成效果之前,我觉得我们还有必要认识一下Addictive Animation
。
Addictive Animation
说是可加动画
,但其实是在先做减法再做加法。
首先,我们会有一个Base Pose------比如说向前跑
,然后我们使用当前动画身体前倾得向前跑
。
那我们拿身体前倾得向前跑
-向前跑
= 身体前倾
。
此时,我们把这个可加动画直接应用于持枪向前跑
,那我们就可以得到身体前倾得持枪向前跑
。
还记得我们之前在做Equip Animation时的Lean参数吗,当时我们为了得到每个动作的Lean,把每一个动作都做了一个左右倾斜的副本用在BlendSpace时,但其实那样的效果不仅麻烦,而且还很差。如果我们做两个Addictive Animation,分别是向左倾斜向前跑
, 向右倾斜向前跑
,把它们的Base Pose
设定为向前跑
,接着再把这两个动作做成Blend Space
应用于向后跑
,向右跑
等动作,我们就会得到倾斜着向后跑
,倾斜着向右跑
等动作。很合理吧?
制作 AimOffset Asset
制作AimOffset的素材,我们需要每个动作的Addictive Animation
。
在UE提供的Animation Pack中,我们可以对现成的Animation Sequence进行切片来得到
只要在你想切除的特定的帧,切除之前帧再切除之后帧即可,只保留当前帧。
最后,我们得到各个方向的动画,每个动画只有一帧:
接着我们对每个动画进行Addictive Type进行批量设置:
这里的Base Pose统一都是角色的正前方,也就是Center Center
接着我们开始制作AimOffset Space
横轴为Yaw即角色瞄准的水平旋转,纵轴为角色瞄准的纵轴旋转。
。
获取角色瞄准的Yaw和Pitch
首先我们要确定AOYaw,当我们角色在奔跑时,不需要瞄准偏移,所以我们只有在原地不动时才会有AO_Yaw的需求,而这个Yaw总是代表的瞄准方向和角色瞄准方向的偏移。同时,我们都知道这个Yaw和Pitch都是时刻更新的,所以我们尽量不去使用Replicated系统,避免客户端和Server的信息交换占用太多带宽,然而可惜的是,我发现Yaw不得不由Server进行转发,因为它似乎并没有和Pitch一样UE已经帮我们同步好了。
c++
// BlasterCharacter.h
UPROPERTY(Replicated)
float AO_Yaw;
float AO_Pitch;
FRotator StartAimRotation;
FRotator CurrentAimRotation;
FRotator DeltaRotation;
// 计算 Aim Yaw 和 Aim Pitch
void CalcAimOffset(float DeltaSeconds);
// BlasterCharacter.cpp
void ABlasterCharacter::CalcAimOffset(float DeltaSeconds)
{
// 如果手中没有武器 直接退出
if (!EquipWeaponModule || !EquipWeaponModule->GetEquippedWeapon())
return;
auto Speed = FVector(GetVelocity().X, GetVelocity().Y, 0.f).Size();
auto IsInAir = this->GetCharacterMovement()->IsFalling();
// 当角色不移动时
if (Speed < 3 && !IsInAir)
{
// 将Yaw交给Server去更新
if (HasAuthority())
{
CurrentAimRotation = FRotator(0, GetBaseAimRotation().Yaw, 0);
auto ActorYaw = StartAimRotation.Yaw;
auto DeltaRot = UKismetMathLibrary::NormalizedDeltaRotator(StartAimRotation, CurrentAimRotation);
this->AO_Yaw = DeltaRot.Yaw;
}
this->bUseControllerRotationYaw = false;
}
// 在空中时或者在步行时 得到当前瞄准方向
if (Speed >= 3 || IsInAir)
{
if (HasAuthority())
{
StartAimRotation = FRotator(0, GetBaseAimRotation().Yaw, 0);
this->AO_Yaw = 0;
}
this->bUseControllerRotationYaw = true;
}
this->AO_Pitch = GetBaseAimRotation().Pitch;
// 如果Pitch大于90度,那么我们需要将其转换为-90到0度 这是因为UE的打包压缩算法 会把负值转化为无符号数
if (AO_Pitch > 90 && !IsLocallyControlled())
{
FVector2D InRange = FVector2D(270, 360);
FVector2D OutRange = FVector2D(-90, 0);
AO_Pitch = FMath::GetMappedRangeValueClamped(InRange, OutRange, AO_Pitch);
}
}
void ABlasterCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> &OutLifetimeProps) const
{
// ...
// 这里的属性是在客户端和服务器之间进行同步的
DOREPLIFETIME(ABlasterCharacter, AO_Yaw);
}
void ABlasterCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
CalcAimOffset(DeltaTime);
}
你会注意到,我们的AO_Yaw是由当前的角色的瞄准方向和角色移动完最后停留的完整方向构成的,同时如果角色没有武器,该逻辑就不进行,这样一来就会有隐藏的问题: 角色如果在不装备武器的状态下到装备武器,那么StartAimRotation会是0,这意味着AO_Yaw会在装备上枪的那一刻变得很大,但其实我们期望是0才对。解决方案其实很简单,一种是那我在角色装备上武器函数里更新StartAimRotation为当前的AimRotation或者直接用ActorRotation(其实我个人觉得这个才是正解)。
动画蓝图
那么接下来我们开始构建动画蓝图,我们在一开始说过,AimOffset的本质是一个Addictive Animation,它需要基于Base Pose才能发挥作用,同时,我们还需要保证角色下半身动作不会受到AimOffset动画的影响(有待试验,我感觉没啥区别)。