오늘 구현할 것
캐릭터가 Idle 상태일 때, 90까지는 캐릭터의 머리가 회전하고, 90이상일 때는 Turn 애니메이션을 출력하며 캐릭터의 방향을 전환해주어야 한다.
왼쪽과 오른쪽으로 도는 애니메이션은 있지만, Idle 상태에서 캐릭터의 얼굴이 돌아가는 애니메이션이 없어서, 기존의 Idle 애니메이션을 편집해주기로 했다.
참고 영상
https://dev.epicgames.com/community/learning/tutorials/j4DO/unreal-engine-175bdc
먼저 Idle 애니메이션을 열고, 컨트롤 릭을 구워야 한다.
구워주면 이렇게 시퀀서 탭이 생긴다.
요 Auto 키를 활성화하고 애니메이팅을 시작해주면 된다.
나는 고개만 회전시킬 것이기 때문에, 고개를 돌렸을 때 영향이 가는 그래프만 건들여주려고 한다. 그래프의 맨 처음과 끝, 중간중간의 키프레임에 가서 고개를 90도 돌려준 후, 중간 키프레임을 삭제하고 자연스럽게 곡선을 만들어주자 :)
나는 높이가 확연히 차이나는 부분들에 키프레임을 찍어주었고, 중간 프레임 삭제 후 Cube interpolation - Automatic tangents(1) 을 사용해 곡선을 부드럽게 만들어 주었다.
그럼 이렇게 왼쪽으로 고개가 돌아간 애니메이션이 완성된다!
Bake Animation Sequence를 누른 후, 경로와 이름을 지정해주고 익스포트 하면 시퀀스가 생긴다. 같은 방법으로 오른쪽을 쳐다보는 애니메이션 시퀀스도 만들어주었다.
기존의 블렌드 스페이스에 각도에 따른 Idle 애니메이션을 넣어주었다. 이제 캐릭터가 쳐다보는 쪽의 Angle을 넣어주어야 한다.
if (Player->GetControlState() == ECharacterControlType::Idle)
{
Angle = Player->GetLookRotation();
}
else
{
Angle = CalculateDirection(Player->GetVelocity(), Player->GetActorRotation());
}
AnimInstance의 NativeUpdateAnimation()
함수에서, Idle 상태일 때만 캐릭터의 Look Rotation을 받아오도록 분기를 넣어주었다.
float ATrapperPlayer::GetLookRotation() const
{
FVector ForwardVector = GetActorForwardVector();
FVector LookVector = GetControlRotation().Vector();
LookVector.Normalize();
float DotResult = FVector::DotProduct(ForwardVector, LookVector);
float Angle = FMath::RadiansToDegrees(FMath::Acos(DotResult));
FVector CrossResult = FVector::CrossProduct(ForwardVector, LookVector);
if (CrossResult.Z < 0)
{
Angle *= -1.f;
}
UE_LOG(LogTemp, Warning, TEXT("Angle : %f"), Angle);
// 컨트롤러의 회전 방향을 향해 쳐다보는게 자연스러워서 반전
return -Angle;
}
앞을 향해 서있는 캐릭터의 전방벡터와 컨트롤러의 벡터를 내적 / 외적하여 각도와 방향을 찾아주었다. 값에 -
를 곱해 리턴해주는데, 캐릭터가 컨트롤러의 회전방향을 향해 쳐다보는게 더 자연스러울 것 같아 반전시켜주었다.
그럼 이렇게 컨트롤러에 회전에 따라 캐릭터의 고개가 회전하는 것을 볼 수 있다.
역시는 역시.. 문제가 없을리가 없다! 특정 구간에서 회전값이 뚝뚝 끊겨 반전되버리는 현상을 확인했다.
UE_LOG(LogTemp, Warning, TEXT("Value : %f / %f"), CrossResult.Z, Value);
LogTemp: Warning: Value : -0.005940 / 13.479462
LogTemp: Warning: Value : -0.005940 / 13.479462
LogTemp: Warning: Value : -0.002970 / 13.476121
LogTemp: Warning: Value : -0.002970 / 13.476121
LogTemp: Warning: Value : 0.000000 / -13.474993
LogTemp: Warning: Value : 0.002970 / -13.476121
LogTemp: Warning: Value : 0.005940 / -13.479462
13
-> -13
으로 널뛰기 해버린다... 도대체 왜??.. 어디부터 값이 이상한건지 계산에 쓰이는 값들을 찍어봤다.
LogTemp: Warning: Value : -0.042079
LogTemp: Warning: Value : -0.042079
LogTemp: Warning: Value : -0.033066
LogTemp: Warning: Value : -0.027040
LogTemp: Warning: Value : -0.021021
LogTemp: Warning: Value : -0.012006
LogTemp: Warning: Value : -0.003000
LogTemp: Warning: Value : 0.005999
LogTemp: Warning: Value : 0.011992
LogTemp: Warning: Value : 0.017987
LogTemp: Warning: Value : 0.020971
LogTemp: Warning: Value : 0.023952
LogTemp: Warning: Value : 0.026946
LogTemp: Warning: Value : 0.026929
추측을 해봤는데, 두 벡터가 거의 평행하면 외적의 크기가 매우 작아지면서 차이가 커지는게 아닐까.... (실은 잘 모르겠음) 거의 평행하다는게 각도가 어느정도부터인건지 모르겠는데..
float ATrapperPlayer::GetLookRotation()
{
FVector ForwardVector = GetActorForwardVector().GetSafeNormal();
FVector LookVector = GetControlRotation().Vector().GetSafeNormal();
float DotResult = FVector::DotProduct(ForwardVector, LookVector);
float Angle = UKismetMathLibrary::DegAcos(DotResult);
FVector CrossResult = FVector::CrossProduct(ForwardVector, LookVector);
// 컨트롤러의 회전 방향을 향해 쳐다보는게 자연스러워서 반전
float Sign = -UKismetMathLibrary::SignOfFloat(CrossResult.Z);
float SmoothedValue = FMath::Lerp(PreviousAngle, Angle * Sign, LookSmoothFactor);
PreviousAngle = SmoothedValue;
return SmoothedValue;
}
정말!! 도저히 모르겠어서 그냥 보간해버렸다!!!! 누구라도 알면 제발 알려주세요..
자연스럽다고 해줘
이제 컨트롤러의 회전이 -90, 90도를 넘겼을 때 캐릭터가 돌아서는 애니메이션을 넣어주어야 한다.
몽타주는 두개의 섹션으로 LeftTurn, RightTurn 이렇게 두개 만들어주었다.
Turn 몽타주 재생은 아까 AnimInstance에서 GetLookRotation()
함수를 통해 체크 하도록 했다. 뭔가 설계적인 부분에서 마음에는 안들었지만, 일단은 진행!
float ATrapperPlayer::GetLookRotation()
{
FVector ForwardVector = GetActorForwardVector().GetSafeNormal();
FVector LookVector = GetControlRotation().Vector().GetSafeNormal();
float DotResult = FVector::DotProduct(ForwardVector, LookVector);
float Angle = UKismetMathLibrary::DegAcos(DotResult);
FVector CrossResult = FVector::CrossProduct(ForwardVector, LookVector);
float Sign = UKismetMathLibrary::SignOfFloat(CrossResult.Z);
float SmoothedValue = FMath::Clamp(FMath::Lerp(PreviousAngle, Angle * Sign, LookSmoothFactor), -90, 90);
if ((SmoothedValue <= -90 || SmoothedValue >= 90) && !bTurnMontageTrigger)
{
PlayTurnMontage(SmoothedValue);
SmoothedValue = PreviousAngle;
UE_LOG(LogTemp, Warning, TEXT("Angle %f"), SmoothedValue);
}
PreviousAngle = SmoothedValue;
// 컨트롤러의 회전 방향을 향해 쳐다보는게 자연스러워서 반전
return -SmoothedValue;
}
SmoothedValue 값이 -90보다 작거나, 90보다 크고 몽타주가 재생중이지 않을 때 몽타주를 재생하도록 했다.
void ATrapperPlayer::PlayTurnMontage(float Angle)
{
if (HasAuthority())
{
if (Angle <= -90)
{
MulticastTurnMontage(true);
}
else if (Angle >= 90)
{
MulticastTurnMontage(false);
}
}
else if (IsLocallyControlled())
{
if (Angle <= -90)
{
ServerRPCTurnMontage(true);
}
else if (Angle >= 90)
{
ServerRPCTurnMontage(false);
}
}
bTurnMontageTrigger = true;
UE_LOG(LogTemp, Warning, TEXT("Play Turn Montage %f"), Angle);
}
void ATrapperPlayer::ServerRPCTurnMontage_Implementation(bool IsLeft)
{
if (IsLeft)
{
MulticastTurnMontage(true);
}
else
{
MulticastTurnMontage(false);
}
}
void ATrapperPlayer::MulticastTurnMontage_Implementation(bool IsLeft)
{
if (!TurnAnimationMontage) return;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (IsLeft)
{
AnimInstance->Montage_Play(TurnAnimationMontage);
FOnMontageEnded EndDelegate;
EndDelegate.BindUObject(this, &ATrapperPlayer::OnTurnMontageEnded);
AnimInstance->Montage_SetEndDelegate(EndDelegate, TurnAnimationMontage);
AnimInstance->Montage_JumpToSection(FName("LeftTurn"), TurnAnimationMontage);
RecentTurnDirection = -90.f;
UE_LOG(LogTemp, Warning, TEXT("Trun Left"));
}
else
{
AnimInstance->Montage_Play(TurnAnimationMontage);
FOnMontageEnded EndDelegate;
EndDelegate.BindUObject(this, &ATrapperPlayer::OnTurnMontageEnded);
AnimInstance->Montage_SetEndDelegate(EndDelegate, TurnAnimationMontage);
AnimInstance->Montage_JumpToSection(FName("RightTurn"), TurnAnimationMontage);
RecentTurnDirection = 90.f;
UE_LOG(LogTemp, Warning, TEXT("Trun Right"));
}
}
인자로 각도를 받고, Authority인지 Autonomous인지 / 왼쪽인지 오른쪽인지 분기를 나누어 네트워크 처리(애니메이션 재생)를 한 뒤 몽타주가 진행중이라는 bTurnMontageTrigger
를 true
로 만들어 주었다.
델리게이트 바인딩이 중간에 껴있는데, BeginPlay()
함수에서 바인딩을 진행했더니 게임을 시작할 때마다 자꾸 한번씩 End 델리게이트가 실행되버리는 현상이 있었다. 마침 옆에 이걸로 삽질했던 친구가 있어서 물어보니, 사용할 때마다 바인딩해주면 정상적으로 작동한다고 한다. 정말 이해가 안됐지만 잘 동작했기 때문에, 우선은 사용할 때마다 바인딩해주는 식으로 해놨다. 나중에 이유를 다시 찾아볼 예정!
void ATrapperPlayer::OnTurnMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
bTurnMontageTrigger = false;
SetActorRotation(FRotator(0, GetActorRotation().Yaw + RecentTurnDirection, 0));
UE_LOG(LogTemp, Warning, TEXT("MontageEnded"));
}
RecentTurnDirection
변수에 캐릭터가 회전해야 하는 각도를 넣어주고, 몽타주가 끝났을 때 호출되는 델리게이트에서 캐릭터의 회전 값을 설정해주었다.
우선 구현은 됐는데, 우선 애니메이션도 Idle과 잘 맞지 않을 뿐더러 컨트롤러 회전값이 확 변하거나 하면 이상하게 동작하고, 몽타주가 끝난 뒤 화면이 한번씩 깜빡거리는 문제도 있다.
근데 우선, 이 Idle 자세에서 Turn 애니메이션이 나오는 것 자체가 우리 게임에서 어색하다는 생각이 들었다. 물론 이 동작을 많이 다듬어야 하기도 하고, 플레이 테스트가 많이 이루어진 뒤 판단하는게 더 맞긴 하지만, 우리 게임 특성상 뭔가를 찾거나 설치하기 위해 가만히 서서 마우스 회전을 하는 일이 많을 것 같은데 그때마다 이 애니메이션이 출력되는게 뭔가 이상할 것 같다는 느낌이 들었고, 기획자님께 동작을 보여드리고 문의를 드렸다.
결국 Turn이 들어가지 않은 상태가 더 자연스럽고 나을 것 같다는 결론이 나왔고, 구현했던 부분들을 제거하기로 했다.