개선된 구르기 동작

목표

  • 타겟팅 상태에서의 이동 및 구르기 동작을 개선한다.
  • 타겟을 바라보면서도, 플레이어가 입력한 방향으로 구르는 동작을 수행한다.

기존 동작의 문제점

이전 타겟팅 시스템 포스트에서 언급한 문제점은 다음과 같다.

  • 타겟팅 중임에도 공격 방향과 이동 방향이 일치하지 않아 공격이 제대로 맞지 않는다.
  • 캐릭터 회전을 제어하는 요소가 많아 방향 제어가 복잡하다.

위 문제를 해결하기 위하여 구르기 동작을 다음과 같이 수정하였다.

캐릭터는 오로지 타겟만을 바라본다

기존에는 캐릭터가 컨트롤러의 회전을 사용하는 옵션인 bUseControllRotationYaw를 false로 설정하고, 이동 함수인 BasicMove에서 캐릭터를 이동 방향으로 회전시켰다.

이제는 이 값을 true로 설정하여 컨트롤러의 회전과 캐릭터의 회전을 일치시켰고, BasicMove 내 회전 제어 로직은 대부분 제거하였다. 다만 타겟팅 시작 시 부드럽게 타겟 방향으로 회전하기 위한 처리는 남겨두었다.

타겟팅이 시작되면 캐릭터는 컨트롤러가 바라보는 방향으로 부드럽게 회전하며, 캐릭터가 바라보는 방향과 컨트롤러가 바라보는 방향의 차이가 일정 각도 이하로 줄어들었을 때부터 컨트롤러의 회전을 이어받아 사용한다.

// IVPlayerCharacter.cpp
void AIVPlayerCharacter::BasicMove(const FInputActionValue& Value)
{
	...
		// 타겟팅 시 회전 방향 설정
		if (CharacterStatComponent && CharacterStatComponent->GetCharacterTargetingState() == ETargetingState::OnTargeting)
		{
			// 이미 컨트롤러 회전을 그대로 사용중이면 진행 X
			if (bUseControllerRotationYaw) return;

			// 캐릭터 방향을 컨트롤러 방향에 맞춰 회전
			float ControllerYaw = Rotation.Yaw;
			float CharacterYaw = GetActorRotation().Yaw;
			float InterpYaw = FMath::FInterpTo(CharacterYaw, ControllerYaw, GetWorld()->GetDeltaSeconds(), 5.0f);
			SetActorRotation(FRotator(0.0f, InterpYaw, 0.0f));

			// 회전이 어느 정도 동일해지면 컨트롤러 회전 사용 시작
			float DeltaYaw = FMath::Abs(FMath::FindDeltaAngleDegrees(CharacterYaw, ControllerYaw));
			if (DeltaYaw < 10.0f)
			{
				bUseControllerRotationYaw = true;
			}
		}
	...
}

방향별 구르기 동작?

Untitled

위 이미지에서, 플레이어가 앞 방향으로 구르기를 입력했다면 캐릭터는 어디로 굴러야할까?

  • 플레이어 컨트롤러가 바라보는 방향
  • 캐릭터가 바라보는 방향

보통은 플레이어 컨트롤러가 바라보는 방향으로 구르는 것을 선택한다. 그렇다면 어떻게 캐릭터가 해당 방향으로 구르도록 할 수 있을까? 생각나는 방법은 세 가지였다.

  1. 모션 워핑 사용

    구르기 동작 초반의 짧은 구간 동안 모션 워핑을 적용하여 컨트롤러가 바라보는 방향으로 회전시킨다. 앞구르기 애니메이션 하나로 커버가 가능하다는 장점이 있지만, 짧은 시간 내에 회전하면서 구르는 모습이 어색하기에 포기했다.

  2. 미리 회전한 뒤 구르기

    구르기 전에 미리 해당 방향으로 캐릭터를 회전시킨다. BasicMove에서처럼 회전 각도가 일정 조건을 만족했을 때 구르기를 실행한다. 이 또한 앞구르기 애니메이션 하나로 커버가 가능하다는 장점이 있지만, 회전하는 시간 때문에 구르기 발동이 지연되어 회피 용도로는 부적합했다. 그래서 패스.

  3. 방향별 구르기 애니메이션 사용

    캐릭터를 기준으로 이동 입력 방향을 계산하고, 이 방향에 해당하는 구르기 애니메이션을 선택한다. 위 이미지 기준으로는 왼쪽 구르기가 실행되어 움직임 입력 방향으로 이동할 것이다. 이는 4방향 구르기 애니메이션이 필요하다는 단점이 있지만, 빠른 반응속도와 자연스러운 동작이 가능하다.

결국 방향별 구르기 애니메이션을 선택했다. 2D 블랜드 스페이스를 사용하면 대각선 방향도 부드럽게 움직일 수 있지 않을까 생각했지만, 테스트해보니 어색한 결과물이 나왔기에 그냥 4방향 애니메이션만 사용했다.

방향별 동작 선택하기

이전 피격 방향별 애니메이션 재생 기능을 만들 때 사용했던 방식과 비슷하다. 캐릭터의 정면 방향과 움직임 입력 방향을 내적하여 4개의 방향 중 하나를 선택하는 것이다.

Untitled

이 두 방향은 단위 벡터이기 때문에 오로지 cos()에 의하여 값이 결정된다. 대각선으로 90도씩의 범위를 담당할 때, cos(45°) ≈ 0.707 값을 기준으로 방향을 계산한다. 내적은 정면과 측면 벡터에 대해 진행한다. 방향 판단시 전후 판단이 우선되며, 이들의 값이 (-0.707, 0.707) 범위 내부여서 측면 이동 성향이 더 강할 때 좌우 판단이 진행된다.

// IVPlayerCharacter.cpp
void AIVPlayerCharacter::SpecialMove(const FInputActionValue& Value)
{
	// 이미 특수 동작 중이거나 공중에 떠 있을 때는 새로운 동작을 실행하지 않는다.
	bool bInAir = GetCharacterMovement()->IsFalling();
	bool bInSpecialMove = CharacterStatComponent->GetCharacterSpecialMovementState() != ESpecialMovementState::None;

	if (!bInAir && !bInSpecialMove)
	{
		if (AnimInstance && CharacterStatComponent)
		{
			// 입력 방향과 캐릭터 방향을 기준으로 어느 구르기 동작이 적합한지 판단
			FVector CharForward = GetActorForwardVector();
			FVector CharRight = GetActorRightVector();

			float FowardDot = FVector::DotProduct(CharForward, MoveDirection);
			float RightDot = FVector::DotProduct(CharRight, MoveDirection);

			float RollX = FMath::Clamp(FowardDot, -1.0f, 1.0f); // 이미 -1 ~ 1 사이 값이지만 혹시 모르니 한번 더 클램핑
			float RollY = FMath::Clamp(RightDot, -1.0f, 1.0f);

			// 방향 별 구르기 몽타주 선택
			UAnimMontage* SelectedRollMontage = nullptr;
			if (RollX >= 0.707) // cos(45°) ≈ 0.707
			{
				SelectedRollMontage = RollFrontMontage;
			}
			else if (RollX <= -0.707)
			{
				SelectedRollMontage = RollBackMontage;
			}
			else if (RollY > 0.0) // 부호만 확인하면 된다
			{
				SelectedRollMontage = RollRightMontage;
			}
			else if (RollY < 0.0)
			{
				SelectedRollMontage = RollLeftMontage;
			}

			// 구르기 몽타주 재생
			AnimInstance->Montage_Play(SelectedRollMontage);

			// 구르기 시작 및 종료 시 상태 변경
			CharacterStatComponent->SetCharacterSpecialMoveState(ESpecialMovementState::Rolling); // 구르기 시작할 때 상태 변경
			FOnMontageBlendingOutStarted BlendingOutDelegate;
			BlendingOutDelegate.BindLambda([this](UAnimMontage* Montage, bool bInterrupted)
				{
					CharacterStatComponent->SetCharacterSpecialMoveState(ESpecialMovementState::None); // 구르기 끝날 때 상태 변경
				});
			AnimInstance->Montage_SetBlendingOutDelegate(BlendingOutDelegate, SelectedRollMontage);
		}
	}
}

결과물

Untitled