Class GameDev* SheepAdult

[Unreal Engine 4.27 C++] Change Direction (방향 전환) Animation 본문

Unreal Project

[Unreal Engine 4.27 C++] Change Direction (방향 전환) Animation

SheepAdult 2022. 8. 9. 01:07

 (상수 같은 경우 코드의 가시성을 높이기 위해 따로 빼두지 않고 넣어놨습니다.)

 3D 게임 개발 시 간단하게 이동을 구현하면(기본 언리얼이 제공하는 3D 콘텐츠 팩 같은 경우), 좌측으로 이동했다가 우측으로 이동할 경우 특별한 마찰력 등의 저항을 주지 않았다면 짧은 시간 안에 속도가 0으로 줄어들었다가 몸이 반대로 돌아가며 속도가 다시 서서히 올라간다. 방향 전환이 이루어질 때 아무런 애니메이션 없이 휙 돌아가는 모션이 없어 어색해 약간 주춤(?)하는(자동차 드리프트 느낌) 애니메이션을 넣어 보고자 했다.

(코드는 마지막에)

 

1. 구현에서의 첫 번째 오판

- 판단

 처음에 생각했을 때 캐릭터의 AxisValue를 받아서 구현해 보고자 했다. 일단, 속도가 너무 느리면, 즉 걸을 때는 일반적인 경우처럼 하기로 했고 달리는 중 어느정도 속도가 빨라지면 드리프트가 발생시켜야겠다고 생각했다. 캐릭터의 Velocity와 AxisValue의 값이 1에서 -1로 바뀌는 순간 이벤트가 발생할 수 있는 boolean값을 true로 바뀌면 어떨까 생각했다.

 

-문제점

 하지만 실제로 코드를 돌려보니 A키를 입력하다가 D키를 입력했을 때(해당 게임은 wasd로 이동한다.) AxisValue가 0이 나온다. 1과 -1이 더해지는 경우가 있기 때문이다,, 그래서 생각을 바꿔봤다.

 

2. 구현에서의 두 번째 오판

- 판단

 (코드를 지웠다 썻다해서 정확히 기억은 나지 않지만) AxisValue의 값과 Character의 ForwardVector를 내적해서 방향 전환을 판단하기로 했다. AxisValue X값과 Y값의 Vector2D와 ForwardVector의 X, Y값의 Vector2D값을 내적하여 일정 값으로 구분지어서 결정하기로 했다. 하지만 언제나 이런 이벤트가 발생하면 안 되므로 Velocity가 일정 수준 이상일 때, 그리고 AxisValue가 0일 때(보통은 0이 나와서 그렇게 했다.)와 동시에 축에 해당하는 이동 키 두 개가 모두 눌려있을 때 이벤트가 발생하도록 했다. 그랬더니 어느 정도 동작은 했다. 그래서 애니메이션을 넣어봤다.

 

-문제점

2-1) 애니메이션의 첫 번째 문제점

 경험상 단타적인 애니메이션이기도 하고 상태가 지속될 것 같진 않아서 몽타주를 사용하기로 했다. 애니메이션은 Mixamo의 Change Direction을 사용하기로 했다. 아래의 애니메이션을 잘라서 사용했다.

Mixamo Animation

하지만 생각보다 애니메이션이 너무 역동적인 느낌도 있었고 어색한 부분이 많았다. 그리고 키에서 손을 떼면 순간적으로 애니메이션이 종료되게 하면 좋을 것 같아서 몽타주가 아닌 애니메이션에 넣어주면 어떨까 생각했다. 하지만 실제로 바꾸고 나니 다른 문제가 생겼다,, 바로 애니메이션에서 스켈레탈 메쉬의 로테이션이 마치 돌아가는 것처럼 보이는데 애니메이션으로 사용하면 캐릭터가 돌아가고 애니메이션도 돌아가 애니메이션이 원하는 방향의 반대 방향에서 끝나는 것과 같은 결과가 나온것이다. 이를 해결하기 위해 애니메이션의 루트 본을 회전시켜 회전 정도를 상쇄시키려 했으나 실패하고 다시 몽타주로 돌아왔다... 그리고 문제점 하나를 더 발견했다.

 

2-2) 애니메이션의 두 번째 문제점

 바로 반시계 방향으로 회전 시의 애니메이션 문제였다. 우리가 가지고 있는 애니메이션은 하나라서 시계 방향 회전은 어느 정도 나온다고 쳐도 반시계 방향 회전에서 우리가 원하는 반대 방향으로(45도 정도) 애니메이션이 실행되고 우리가 원하는 방향으로(225도 정도) 돌기 때문에 굉장히 어색한 애니메이션이 연출되었다.

어색어색

그래서 애니메이션 반대 방향도 믹 사모에서 다운로드하고 시계 반대방향으로 회전할 때를 구분해서 구현하기로 했다. 그리고 구현에서의 문제점이 테스트해보면서 느껴진 건데 AxisValue가 0이 나오지 않는 경우엔 발생하지 않아 이벤트가 발생할 때 발생하지 않을 때가 섞여있어 마음에 들지 않았다. 또한 키를 입력할 때 Velocity로 판단을 하려고 하니 애매한 경우가 있었다. 왜냐하면 키의 반대방향을 입력할 때 속도가 순간적으로 줄어드는데 이 부분도 오차 범위가 어느 정도 있다 보니 이벤트 발생이 랜덤으로 나타났다. 그래서 처음부터 다시 생각하기로 했다. 

 

3. 계획 선에서의 성공

 그래서 방식을 처음부터 다시 생각하기로 했다. 키 입력 시 달리고 있는 상태라면 시간을 카운트해 일정 시간 같은 방향으로 달리면 방향 전환 모드가 true가 되며 슬라이딩을 하거나 점프를 하거나 멈추면 이 타이머는 리셋되어야 할 것이다. 그 상태에서 입력 축과 캐릭터의 ForwardVector를 외적해 시계 방향인지 반시계 방향인지 구별을 해 애니메이션 두 개 중 어떤 것을 사용해야 할지 정해주면 될 것 같아 구현을 했다. Axis 입력에서부터 생각했다. 먼저 대각선 이동은 고려하지 않고 X축 이동, Y축 이동에 대해서만 생각했다. 언리얼은 기본적으로 양 옆으로 이동시 안쪽으로 회전했다.(물론 카메라를 어디에 두느냐에 따라 다르다.)

UE4 캐릭터 회전 방향

x, y축이 위의 그림과 같다면 양 옆 키를 눌렀을 때 위와 같이 메쉬가 회전한다. 일단 이를 바꿔주고 싶었다. 그래서 A키를 누르다가 D키를 누르면 Yaw Rotation값을 1을 빼줘 안쪽으로 회전할 수 있도록 바꿨다. W와 S키도 마찬가지로 했다. 코드는 아래와 같다. 아래는 W, S키에 대한 코드이다.

 

// MainCharacter.cpp
// MoveRight(A, D 키)도 대응해 줄 변수 정도만 바꿔서 똑같이 해주면 됨
void AMainCharacter::MoveForward(float AxisValue)
{
    AddMovementInput(Direction, AxisValue);
    if (AxisValue != 0.0f)
    {
    	// 후에 사용할 Axis값저장
        XAxisValue = AxisValue;
        // 후술
        bCanChangeDirectionX = CanChangeDirection(AxisValue, XAxisValue);
        // 위에서 설명한 부분. 회전의 방향을 바꿔주고 싶어서 사용
        if (AxisValue > 0.0f && UKismetMathLibrary::EqualEqual_RotatorRotator(GetActorRotation(), FRotator(0.0f, -180.0f, 0.0f), 1.0f))
        {
            SetActorRotation(FRotator(0.0f, -181.0f, 0.0f));
        }
    }
}

그리고 이제 대각선을 해결할 차례이다. 방향 전환을 할 수 있는지 체크하는 함수를 만들었다. 이는 Axis입력 함수에서 호출된다.

// MainCharacter.cpp
// AxisValue는 이동 함수의 매개변수인 AxisValue이고 Axis는 저장해놓은 바로 이전 프레임의 Axis 값이다.
bool AMainCharacter::CanChangeDirection(float AxisValue, float Axis)
{
	// Axis축 값이 이전과 같을 때(일정 키 계속 누를 때) && 속도가 어느정도 붙었을 때(아직 미숙한 부분)
	if (UKismetMathLibrary::NearlyEqual_FloatFloat(AxisValue, Axis) && GetVelocity().Size() > 250.0f)
	{
    	// 카운트가 0부터 들어감
		UWorld* World = GetWorld();
		ChangeDirectionTime += UGameplayStatics::GetWorldDeltaSeconds(World);
		return false;
	}
    // 만약 방향전환이 이루어 지면
	else
	{
    	// 달리는 상태가 아니면 시간0으로 초기화
        // 해당 부분도 조금 틀어막은 느낌이 있음,, (!bIsSprinting은 shift눌러서 달릴 때 발생)
		if (!bIsSprinting)
		{
			ChangeDirectionTime = 0.0f;
		}
        // 위에서 설명한 내적을 이용하여 방향 전환이 이루어지며 일정 시간을 초과 했을 때
		FVector2D Vector1 = FVector2D(GetActorForwardVector().X, GetActorForwardVector().Y);
		FVector2D Vector2 = FVector2D(XAxisValue, YAxisValue);
		if (ChangeDirectionTime > 0.7f && UKismetMathLibrary::DotProduct2D(Vector1, Vector2) < 0.0f)
		{
        	// 시간 초기화 후 true 반환
			ChangeDirectionTime = 0.0f;
			return true;
		}
		else
		{
        	// 달리다가 걷는 상태에서 방향 전환시 이벤트 발생을 막기위해 
			if (GetVelocity().Size() < 60.0f)
			{
				ChangeDirectionTime = 0.0f;
			}
			return false;
		}
	}
}

그 후 위에서 나왔듯이 입력 축에 코드 작성

 

이제 Tick() 함수에서 호출되는 반향 전환 코드이다.

// MainCharacter.cpp
void AMainCharacter::ChangeDirection()
{	
	// X축이나 Y축 이동 시 방향전환 이벤트가 발생할 조건이 충족되었을 경우
	if (bCanChangeDirectionX || bCanChangeDirectionY)
	{
		FVector2D Vector1 = UKismetMathLibrary::MakeVector2D(GetActorForwardVector().X, GetActorForwardVector().Y);
		FVector2D Vector2 = UKismetMathLibrary::MakeVector2D(XAxisValue, YAxisValue);
        // 위에서 시계, 반시계 방향 중 한 방향에서 문제가 나타나므로 두 벡터를 외적하여 시계 방향으로
        // 회전하는 지, 반시계 방향으로 회전하는지 판단. 그에 맞는 애니메이션 몽타주를 실행한다.
        // 두 벡터의 외적이 양수면 반시계 방향, 음수면 시계 방향이다.
		float CrossProduct = UKismetMathLibrary::CrossProduct2D(Vector1, Vector2);
		if (CrossProduct >= 0.0f)
		{
			PlayAnimMontage(M_ChangeDirection_Mirror);
		}
		else
		{
			PlayAnimMontage(M_ChangeDirection);
		}
	}
}

이제 얼추 구현을 됐다. 이제 몽타주 실행 시 키보드에서 손을 떼면 지직(?) 거리는 문제가 발생해 이를 해결하고자 Additive Animation을 추가해줬다. 이도 사실 좋은 방법인지는 모르겠으나 구글링 끝에 사용했다,,

몽타주가 아닌 애니메이션 파일에서 

위의 설정처럼 바꿔준다. 베이스 포즈는 아마 그 후의 애니메이션 상태를 넣어주면 될 것이다. 그럼 뚝뚝 끊김 현상이 없어진다.(애니메이션의 변형이 오므로 타협점을 찾아서 사용했다.) 그리고 애니메이션이 너무 과한 것 같아 몽타주 파일에서 블렌드 아웃 시간도 바꿔줬다.

조금은 오버스러움이 덜어졌다. 결과는 아래와 같다.

https://www.youtube.com/watch?v=3gE2pqNvnMo 

결론 :

생각보다 오래 걸렸다. 3~4일쯤 소모한 것 같다. 사실 위는 생각나는 시행착오를 작성한 것이지 몇 가지 방법을 더 시도했는데 기억이 잘 나지 않는다,, 물론 이 방법이 잘못된 점은 당연하게 있을 것이며 실제로 원하는 방향의 반대 방향으로 돌아갈 경우가 가끔 있다. 이는 추후에 수정하도록 하고 더 좋은 구현 방법이 떠오르면 구현해 봐야 할 것 같다.