피격 컴포넌트

뭐하는 컴포넌트인가

RPG 프로젝트인 만큼 피격 처리를 담당하는 컴포넌트가 있으면 좋겠다고 생각했다. 피격 후 데미지는 스탯 컴포넌트에서, 피격 반응은 HitReactionComponent에서 처리한다. 주 역할은 다음과 같다.

  • 피격 정보, 오너 정보를 사용해 캐릭터 정면 기준 피격 각도 계산
  • 피격 방향에 따른 피격 애니메이션 재생
  • 오너의 애님 인스턴스, 피격 방향별 반응 몽타주, 피격 리액션 사용 여부 변수와 같은 데이터 관리

피격 각도 계산

  1. 각도 계산 전체 코드

     // HitReactionComponent.cpp
     void UIVHitReactionComponent::ComputeHitAngle(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
     {
     	if (!bIsUsingHitReaction || !GetOwner()) return; // 피격 리액션 사용 여부 및 컴포넌트 연결 확인
        
     	AActor* Owner = GetOwner(); // 피격 대상
     	float Angle = 0.0f;	 
        
     	// 공격 방향 계산
     	if(DamageCauser)
     	{
     		// 이후 연산을 위한 벡터 정규화
     		FVector CharacterFoward = Owner->GetActorForwardVector().GetSafeNormal();
     		FVector HitDirection = (DamageCauser->GetActorLocation() - Owner->GetActorLocation()).GetSafeNormal();
        
     		// 공격 방향 구하기
     		Angle = FMath::RadiansToDegrees(FMath::Acos(FVector::DotProduct(CharacterFoward, HitDirection)));
        
     		// 공격 방향 좌우 판단
     		FVector CrossProduct = FVector::CrossProduct(CharacterFoward, HitDirection);
     		Angle *= (CrossProduct.Z > 0.0f) ? 1.0f : -1.0f;
     	}
        
     	// 방향에 따른 애니메이션 선택
     	if (AnimInstance)
     	{
     		PlayHitReactionMontage(Angle);
     	}
     }
    

    이를 설명함에 있어서 피격 방향, 공격 방향 용어를 혼용함을 미리 알린다.

  2. 벡터 정규화

     // 이후 연산을 위한 벡터 정규화
     FVector CharacterFoward = Owner->GetActorForwardVector().GetSafeNormal();
     FVector HitDirection = (DamageCauser->GetActorLocation() - Owner->GetActorLocation()).GetSafeNormal();
    

    피격 각도 계산을 위해서는 TakeDamage의 데미지 유발자 정보와 컴포넌트 오너 정보가 필요하다. 각각 피격 방향과 캐릭터 정면 방향을 구하기 위함이다. 피격 방향은 데미지 유발자 위치에서 오너의 위치를 빼는 것으로 벡터를 구할 수 있다.

    두 벡터는 GetSafeNormal을 통해 정규화를 거친다. 내적을 위하여 벡터의 크기를 지우고 방향만을 남기는 것이다.

  3. 내적을 사용한 방향 구하기

     // 공격 방향 구하기
     Angle = FMath::RadiansToDegrees(
     	FMath::Acos(
     		FVector::DotProduct(CharacterFoward, HitDirection))); 
    

    두 벡터의 내적은 A dot B = |A||B|cos(theta)과 같이 나타낼 수 있다. 이때, 벡터 A, B는 정규화 된 상태이므로 A dot B = cos(theta)가 된다. [-1, 1] 범위의 cos(theta) 값을 얻었다.

    Acos는 cos의 역함수이다. cos(theta)로부터 theta값, 각도를 알아내는데 사용한다. 얻어낸 각도는 라디안을 사용한다.

    그렇기에 라디안을 일반적으로 사용하는 각도로 변환할 필요가 있다. 그 과정이 RadiansToDegrees다.

    모든 과정을 거친 후, [0, 180] 범위의 각도를 얻을 수 있다.

  4. 외적을 사용한 좌우 판단

     // 공격 방향 좌우 판단
     FVector CrossProduct = FVector::CrossProduct(CharacterFoward, HitDirection);
     Angle *= (CrossProduct.Z > 0.0f) ? 1.0f : -1.0f;
    

    하지만 얻은 각도에는 방향이 없다. 오른쪽, 왼쪽 모두 [0, 180] 범위를 가지기 때문이다. 여기서는 외적을 통해 공격 방향의 좌우 판단을 진행한다.

    외적은 연산 순서에 따라 방향성을 가지며, 오른손 법칙을 사용해 방향을 확인할 수 있다. 외적값이 양수라면 공격자가 오른쪽, 음수라면 왼쪽에서 공격을 받았다고 판단한다.

    왼쪽 피격 시 각도에 음수를 곱하여 [-180, -0]의 범위를 가지도록 한다. 결과적으로 양쪽 360도 범위로 공격 방향을 나타낼 수 있게 되었다. 이렇게 계산한 각도를 기반으로 몽타주 선택을 진행한다.

피격 리액션 몽타주 선택

// IVHitReactionComponent.cpp
void UIVHitReactionComponent::PlayHitReactionMontage(float Angle)
{
	if (!AnimInstance) return;

	UAnimMontage* HitReactionMontage = nullptr; // 재생할 몽타주

	// 각 90도씩 4방향 범위 판단 후 재생할 몽타주 선택
	if (Angle >= -45.0f && Angle < 45.0f)
	{
		HitReactionMontage = FrontHitMontage; // 정면
	}
	else if (Angle >= 45.0f && Angle < 135.0f)
	{
		HitReactionMontage = RightHitMontage; // 오른쪽
	}
	else if (Angle >= -135.0f && Angle < -45.0f)
	{
		HitReactionMontage = LeftHitMontage; // 왼쪽
	}
	else if (Angle >= 135.0f || Angle < -135.0f)
	{
		HitReactionMontage = BackHitMontage; // 뒤쪽
	}

	AnimInstance->Montage_Play(HitReactionMontage); // 몽타주 재생

	// 피격 애니메이션 종료 시 오너의 피격 리액션 종료
	FOnMontageBlendingOutStarted BlendingOutDelegate;
	BlendingOutDelegate.BindLambda([this](UAnimMontage* Montage, bool bInterrupted)
		{
			IIIVHitReactionInterface* HitReactionInterface = Cast<IIIVHitReactionInterface>(GetOwner());
			if (HitReactionInterface)
			{
				HitReactionInterface->EndHitReaction();
			}
		});
	AnimInstance->Montage_SetBlendingOutDelegate(BlendingOutDelegate, HitReactionMontage);
}

피격 방향을 90도씩 분할하여 총 4개의 피격 몽타주를 지정한다. 각 몽타주는 에디터에 노출하여 쉽게 변경할 수 있도록 하였다. 선택된 몽타주 재생 이후, 몽타주가 블랜드 아웃되는 시점에 맞추어 컴포넌트 오너에게 피격 상태가 종료되었음을 알린다.

HitReactionInterface

class IVAN_API IIIVHitReactionInterface
{
	GENERATED_BODY()

public:
	virtual void StartHitReaction() = 0;
	virtual void EndHitReaction() = 0;
};

HitReactionComponent와 함께 사용되는 인터페이스. 컴포넌트 오너는 소유한 컴포넌트에 직접 접근할 수 있지만, 반대로 컴포넌트가 오너의 정보를 참조하려면 오너가 누구인지 알아야한다. 이러한 결합도를 낮추기 위해 도입한 인터페이스이다.

제공하는 두 함수는 캐릭터 상태 변경과 관련된다. 피격 상태 관리는 스탯 컴포넌트가 담당하지만, 이 상태의 변경 시점은 피격 컴포넌트의 몽타주 재생 시간에 종속되어있다. 따라서 몽타주 종료 시점에 맞추어 오너의 상태 변경을 지시한다.