언리얼 엔진 5/개발 일지

[UE5] 블루아카이브 TPS게임 개발일지 (14) - 데미지 판정 구현

ciel45 2023. 12. 25. 15:18

언리얼 엔진의 Actor 클래스에는 데미지를 받는 함수가 기본적으로 정의되어있다.

 

https://docs.unrealengine.com/5.3/en-US/API/Runtime/Engine/GameFramework/AActor/TakeDamage/

 

AActor::TakeDamage

Apply damage to this actor.

docs.unrealengine.com

 

그 시그니쳐는 이러하다.

virtual float TakeDamage
(
    float DamageAmount,
    struct FDamageEvent const & DamageEvent,
    class AController * EventInstigator,
    AActor * DamageCauser
)

 

액터가 데미지를 받게 하고자 한다면, TakeDamage 함수를 오버라이딩하여 사용하면 된다.

 

 

파라미터를 하나씩 살펴보자면,

  • DamageAmount: 문자 그대로 받는 데미지의 양이다.
  • DamageEvent: 데미지에 대한 추가적인 정보를 가지고 있는 구조체이다.
  • EventInstigator: 데미지를 준 컨트롤러이다. 캐릭터가 적에게 데미지를 주었다면, 캐릭터의 컨트롤러를 의미한다.
    • instigator는 '선동자'라는 의미를 가지고 있다.
  • DamageCauser: 데미지를 준 액터 자체이다. 총알이 적에게 박혀 데미지를 주었다면, 그 총알을 의미한다.

 

그리고 float을 리턴하도록 되어있는데, DamageAmount를 리턴하도록 하는 것이 보통이다. 

 

 

 

 

데미지를 받는 함수가 있다면, 데미지를 주는 함수도 있어야 할 것이다.

UGameplayStatics::ApplyDamage 함수가 바로 TakeDamage가 호출되도록 하는 함수이다.

https://docs.unrealengine.com/5.3/en-US/API/Runtime/Engine/Kismet/UGameplayStatics/ApplyDamage/

 

UGameplayStatics::ApplyDamage

Hurts the specified actor with generic damage.

docs.unrealengine.com

static float ApplyDamage
(
    AActor * DamagedActor,
    float BaseDamage,
    AController * EventInstigator,
    AActor * DamageCauser,
    TSubclassOf< class UDamageType > DamageTypeClass
)

 

대부분의 파라미터는 TakeDamage에서와 그 의미가 유사하거나, 비슷한 맥락이다.

 

 

다만 마지막에 TSubclassOf<class UDamageType>이 들어가는데, 이는 UDamageType을 상속하여 커스터마이징한 데미지타입을 넣어줄 수 있음을 의미한다.

 

예를 들면, 지속적으로 데미지를 주는 도트데미지 등을 만들 수 있다.

 

굳이 특별한 데미지타입을 사용할 것이 아니라면 UDamageType::StaticClass()를 넣어주면 된다.

 

 

 

 

 

이제 코드를 작성할 것이다.

Pistol.cpp의 Fire함수에서 적을 맞췄을 때 로그만 출력하는 대신 실제로 ApplyDamage를 호출하도록 하였다.

 

void APistol::Fire(FVector& StartPoint, FVector& Direction)
{
	//UE_LOG(LogTemp, Warning, TEXT("PISTOL FIRE")); //로그에 출력

	FHitResult HitResult;
	FVector EndPoint = StartPoint + Direction * Range;

	if (GetWorld())
	{
		//DrawDebugLine(GetWorld(), StartPoint, EndPoint, FColor::Red, true, -1.f, 0, 0.5f);

		if (GetWorld()->LineTraceSingleByChannel(
			HitResult,
			StartPoint,
			EndPoint,
			ECollisionChannel::ECC_Visibility))
		{
			if (HitResult.GetActor())
			{
				//UE_LOG(LogTemp, Warning, TEXT("%s"), *(HitResult.GetActor())->GetName());
				if (IHitInterface* HitObject = Cast<IHitInterface>(HitResult.GetActor()))
				{
					HitObject->GetHit();
				}

				UGameplayStatics::ApplyDamage(
					HitResult.GetActor(),
					Damage,
					GetInstigator()->GetController(),
					this,
					UDamageType::StaticClass()
				);
			}
		}
	}
}

 

중간의 IHitInterface 사용 코드는 추후에 명중의 효과를 표현하기 위한 것으로, 당장은 사용하지 않고 있다. 인터페이스를 사용한 이유는 적 외에 사물을 명중시켰을 때도 각각에 맞는 효과를 표현하기 위해서이다.

 

 

현재 눈여겨볼 것은 ApplyDamage 부분이다.

2번째 파라미터인 Damage는 미리 만들어둔 float 타입 인스턴스 변수로, 현재 35.0f가 들어가있다.

3번째 파라미터에서는 GetInstigator()를 사용한 것을 볼 수 있다.

 

 

GetInstigator()는 Pawn 포인터를 리턴하는 함수이다.

그러면 Pistol 액터의 Instigator가 언제 Set 되었냐 하면, 캐릭터의 SwitchWeapon 함수 내부에서이다.

void AKannaCharacter::SwitchWeapon()
{
	if(ActionState != EActionState::EAS_Neutral) return;

	if (CharacterState == ECharacterState::ECS_ArmedWithPistol) // 권총 -> 맨손
	{
		CharacterState = ECharacterState::ECS_Unarmed;
		CurrentWeapon = nullptr;
	}
	else if (CharacterState == ECharacterState::ECS_Unarmed) //맨손 -> 권총
	{
		CharacterState = ECharacterState::ECS_ArmedWithPistol;
		CurrentWeapon = WeaponList[0];
	}

	if(CurrentWeapon)
	{
		CurrentWeapon->SetOwner(this);
		CurrentWeapon->SetInstigator(this);
	}

	SetNeutralStateSpeed(); // 중립 상태일 때의 걷기 속도 조정
}

 

무기를 장착할 때 Instigator를 캐릭터로 할당해두었다.

 

 

다시 위의 ApplyDamage를 보면, 파라미터로 넘겨줄 것은 Controller 포인터이므로 GetController()를 호출해주면 된다.

 

 

 

 

이제 ApplyDamage 함수가 완성되었으니, Enemy.cpp로 이동하여 TakeDamage 함수를 오버라이딩할 차례이다.

float AEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	if (Attributes)
	{
		Attributes->ReceiveDamage(DamageAmount);
	}

	UE_LOG(LogTemp, Warning, TEXT("Enemy HP: %f"), Attributes->GetCurrentHealth())

	return DamageAmount;
}

 

앞서 Attribute에 멤버변수로 CurrentHealth, MaxHealth를 만들어두었으므로, 이를 활용하는 함수인 ReceiveDamage()를 만들어 그것을 호출하도록 하였다.

Enemy에서 직접 Attribute->CurrentHealth 등으로 변수에 접근하는 방법도 있지만, 의존성을 낮추기 위하여 별도의 함수를 만든 것이다.

 

그리고 테스트를 위해 UE_LOG 매크로도 달아두었다.

 

 

 

ReceiveDamage 함수는 다음과 같다.

void UAttributeComponent::ReceiveDamage(float Damage)
{
	CurrentHealth = FMath::Clamp(CurrentHealth-Damage, 0.f, 100.f);
}

 

체력이 마이너스가 되지 않기를 바라므로 Clamp 함수를 사용하였다.

 

 

 

다음은 테스트영상이다. 하단 로그에 주목해보자.