본문 바로가기
언리얼_엔진_게임개발_공부/언리얼 C++

[언리얼/C++] ApplyDamage / TakeDamage 파헤쳐보자

by jaboy 2025. 2. 7.

오늘 공부한 내용에서는 캐릭터 클래스에 체력을 구분하고,

지뢰 아이템이 폭발할 때 폭발 범위 내에 있으면 체력을 깎는 (데미지를 입히는) 부분을 구현하였다.

여기에서 두 가지 언리얼 엔진의 코드를 사용했다.

UGameplayStatics::ApplyDamage() - 액터에 데미지를 적용시킨다

AActor::TakeDamage() - 액터가 데미지를 받는다

 

언리얼 엔진의 코드를 사용하게 될 때마다 너무너무 궁금하다!

뜯어?보면 엄청난? 것들이 일어나고 있고 다양한 정보가 오고가기 때문에...

정확히 알아두면 나중에 필요한 정보를 간편하게 뽑아쓸 수 있을 것만 같은 기분...?

그래서 각 함수가 어떻게 정의되어 있는지 살펴보았다.

(비주얼 스튜디오에서 F12를 눌러 함수 정의된 부분으로 순간이동해 살펴보았다. 넘 재밌다. 시간 순삭.)

(아래는 내가 감명 깊게? 살펴본 내용을 이해하기 쉬운 느낌으로 요약한 것이라 일부러 생략한 정보가 훨씬 더 많다.

왜냐? 이것은 당신을 위한 글이 아니다!!!!!!!)

UGameplayStatics::ApplyDamage()

1. DamageTypeClass 변수 값이 실제 damage type 인지 확인한 후 DamagedActor 에 대해 TakeDamage 를 호출하며 DamageEvent 를 전달한다.

float UGameplayStatics::ApplyDamage(AActor* DamagedActor, float BaseDamage, AController* EventInstigator, AActor* DamageCauser, TSubclassOf<UDamageType> DamageTypeClass)
{
	if ( DamagedActor && (BaseDamage != 0.f) )
	{
		// make sure we have a good damage type
		TSubclassOf<UDamageType> const ValidDamageTypeClass = DamageTypeClass ? DamageTypeClass : TSubclassOf<UDamageType>(UDamageType::StaticClass());
		FDamageEvent DamageEvent(ValidDamageTypeClass);

		return DamagedActor->TakeDamage(BaseDamage, DamageEvent, EventInstigator, DamageCauser);
	}

	return 0.f;
}

 

초간단...~

 

2. 여기에서 생성되어 TakeDamage 로 전달되는 DamageEvent 는 FDamageEvent 구조체로 FPointDamageEvent 와 FRadialDamageEvent 가 이를 상속해 각기 다른 대미지의 유형을 세분화한다.

 

/** Damage subclass that handles damage with a single impact location and source direction */
USTRUCT()
struct FPointDamageEvent : public FDamageEvent

// ......

/** Damage subclass that handles damage with a source location and falloff radius */
USTRUCT()
struct FRadialDamageEvent : public FDamageEvent

Point Damage 는 말 그대로 단일 충격 (지점과 방향)

RadialDamage 는 지점으로부터 반경 범위 (거리에 따라 달라지는 데미지)

이 두 가지 대미지이벤트에 대해서는 아래 TakeDamage 를 살펴보며 더 자세히 알아보았다.

AActor::TakeDamage() 와 FPointDamageEvent / FRadialDamageEvent

AActor 클래스의 TakeDamage() 에서 DamageEvent 의 종류에 따라 다르게 실제 데미지를 계산한다.

Point Damage 인 경우

실제 대미지는 결국 DamageAmount 와 같다 (InternalTakePointDamage 가 basically ActualDamage 를 그대로 반환한다)

float ActualDamage = DamageAmount;

UDamageType const* const DamageTypeCDO = DamageEvent.DamageTypeClass ? DamageEvent.DamageTypeClass->GetDefaultObject<UDamageType>() : GetDefault<UDamageType>();
if (DamageEvent.IsOfType(FPointDamageEvent::ClassID))
{
	// point damage event, pass off to helper function
	FPointDamageEvent* const PointDamageEvent = (FPointDamageEvent*) &DamageEvent;
	ActualDamage = InternalTakePointDamage(ActualDamage, *PointDamageEvent, EventInstigator, DamageCauser);

	// K2 notification for this actor
	if (ActualDamage != 0.f)
	{
		ReceivePointDamage(ActualDamage, DamageTypeCDO, PointDamageEvent->HitInfo.ImpactPoint, PointDamageEvent->HitInfo.ImpactNormal, PointDamageEvent->HitInfo.Component.Get(), PointDamageEvent->HitInfo.BoneName, PointDamageEvent->ShotDirection, EventInstigator, DamageCauser, PointDamageEvent->HitInfo);
		OnTakePointDamage.Broadcast(this, ActualDamage, EventInstigator, PointDamageEvent->HitInfo.ImpactPoint, PointDamageEvent->HitInfo.Component.Get(), PointDamageEvent->HitInfo.BoneName, PointDamageEvent->ShotDirection, DamageTypeCDO, DamageCauser);

		// Notify the component
		UPrimitiveComponent* const PrimComp = PointDamageEvent->HitInfo.Component.Get();
		if (PrimComp)
		{
			PrimComp->ReceiveComponentDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
		}
	}
}

 

그런데 주목할 것은 대미지이벤트로부터 Hit 관련해 엄청난 정보가 오고간다는 것!!

충격 지점, 노멀벡터, 피격 컴포넌트, 피격 당한 bone 등등까지...?!!?

해당 액터에 이러한 내용들을 Broadcast 해주고, 컴포넌트에도 별도 함수로 notify 하기 때문에 액터에서 이 정보를 가져와 그에 맞는 대미지 처리를 할 수 있다.

예를 들어 방탄조끼를 입고 있을 때, 사격 당한 컴포넌트가 예를 들어 방탄조끼 컴포넌트라면 대미지를 안 입는 식의 처리도 가능하려나?!

그래서 컴포넌트에서 대미지를 어떻게 받는지 살펴봤다...이 내용은 좀 더 아래에 ㅎㅎ

 

Radial Damage 인 경우

else if (DamageEvent.IsOfType(FRadialDamageEvent::ClassID))
{
	// radial damage event, pass off to helper function
	FRadialDamageEvent* const RadialDamageEvent = (FRadialDamageEvent*) &DamageEvent;
	ActualDamage = InternalTakeRadialDamage(ActualDamage, *RadialDamageEvent, EventInstigator, DamageCauser);

	// K2 notification for this actor
	if (ActualDamage != 0.f)
	{
		FHitResult const& Hit = (RadialDamageEvent->ComponentHits.Num() > 0) ? RadialDamageEvent->ComponentHits[0] : FHitResult();
		ReceiveRadialDamage(ActualDamage, DamageTypeCDO, RadialDamageEvent->Origin, Hit, EventInstigator, DamageCauser);
		OnTakeRadialDamage.Broadcast(this, ActualDamage, DamageTypeCDO, RadialDamageEvent->Origin, Hit, EventInstigator, DamageCauser);

		// add any desired physics impulses to our components
		for (int HitIdx = 0; HitIdx < RadialDamageEvent->ComponentHits.Num(); ++HitIdx)
		{
			FHitResult const& CompHit = RadialDamageEvent->ComponentHits[HitIdx];
			UPrimitiveComponent* const PrimComp = CompHit.Component.Get();
			if (PrimComp && PrimComp->GetOwner() == this)
			{
				PrimComp->ReceiveComponentDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
			}
		}
	}
}

위와 비슷하나 다른 점이 있다면 바로 InternalTakeRadialDamage 함수에서 위와 달리 충격 지점으로부터의 거리에 따라 대미지를 계산한다는 점이다.

Broadcast 에서 Bone 같은 자세한 정보는 전달하지 않는다. (보통 수많은 컴포넌트/본이 대미지 범위에 들어올 것이므로)

 

InternalTakeRadialDamage 함수는 아래와 같은 연산을 한다.

float AActor::InternalTakeRadialDamage(float Damage, FRadialDamageEvent const& RadialDamageEvent, class AController* EventInstigator, class AActor* DamageCauser)
{
	float ActualDamage = Damage;

	FVector ClosestHitLoc(0);

	// find closest component
	// @todo, something more accurate here to account for size of components, e.g. closest point on the component bbox?
	// @todo, sum up damage contribution to each component?
	float ClosestHitDistSq = UE_MAX_FLT;
	for (int32 HitIdx=0; HitIdx<RadialDamageEvent.ComponentHits.Num(); ++HitIdx)
	{
		FHitResult const& Hit = RadialDamageEvent.ComponentHits[HitIdx];
		float const DistSq = (Hit.ImpactPoint - RadialDamageEvent.Origin).SizeSquared();
		if (DistSq < ClosestHitDistSq)
		{
			ClosestHitDistSq = DistSq;
			ClosestHitLoc = Hit.ImpactPoint;
		}
	}

	float const RadialDamageScale = RadialDamageEvent.Params.GetDamageScale( FMath::Sqrt(ClosestHitDistSq) );

	ActualDamage = FMath::Lerp(RadialDamageEvent.Params.MinimumDamage, ActualDamage, FMath::Max(0.f, RadialDamageScale));

	return ActualDamage;
}

...@todo 가 적혀있는 것 너무 귀엽다 ^^ 갑자기 친근한 느낌...

범위 안의 모든 컴포넌트가 Hit 에 해당되기에 배열을 돌며 충격지점에서 가장 가까운 걸 기준으로 거리를 계산하나보다.

Hit의 ImpactPoint 와 RadialDamageEvent 의 Origin 사이의 거리를 기반으로 대미지를 Lerp 하여 실제 대미지로 반환한다.

멋지다... 뭐 하나 대충 하는 게 없다.

 

그럼 위에서 방탄조끼 어쩌구 하며 잠깐 말했던 ReceiveComponentDamage, 즉 컴포넌트 단위에서는 대미지 처리를 어떻게 하는지 살펴보자

ReceiveComponentDamage()

void UPrimitiveComponent::ReceiveComponentDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	if (bApplyImpulseOnDamage)
	{
		UDamageType const* const DamageTypeCDO = DamageEvent.DamageTypeClass ? DamageEvent.DamageTypeClass->GetDefaultObject<UDamageType>() : GetDefault<UDamageType>();
		if (DamageEvent.IsOfType(FPointDamageEvent::ClassID))
		{
			FPointDamageEvent* const PointDamageEvent = (FPointDamageEvent*)&DamageEvent;
			if ((DamageTypeCDO->DamageImpulse > 0.f) && !PointDamageEvent->ShotDirection.IsNearlyZero())
			{
				if (IsSimulatingPhysics(PointDamageEvent->HitInfo.BoneName))
				{
					FVector const ImpulseToApply = PointDamageEvent->ShotDirection.GetSafeNormal() * DamageTypeCDO->DamageImpulse;
					AddImpulseAtLocation(ImpulseToApply, PointDamageEvent->HitInfo.ImpactPoint, PointDamageEvent->HitInfo.BoneName);
				}
			}
		}
		else if (DamageEvent.IsOfType(FRadialDamageEvent::ClassID))
		{
			FRadialDamageEvent* const RadialDamageEvent = (FRadialDamageEvent*)&DamageEvent;
			if (DamageTypeCDO->DamageImpulse > 0.f)
			{
				AddRadialImpulse(RadialDamageEvent->Origin, RadialDamageEvent->Params.OuterRadius, DamageTypeCDO->DamageImpulse, RIF_Linear, DamageTypeCDO->bRadialDamageVelChange);
			}
		}
	}
}

미쳤다~~~

대미지 타입의 CDO(Class Default Object) 를 만들고 이 대미지의 종류 (point / radial 의 ID) 에 따라 각기 다른 Impulse 연산을 한다.

PointDamage 인 경우

포인트 대미지 객체의 Impulse 값, HitInfo의 BoneName, SimulatePhysics on/off 여부를 기반으로 Impulse 벡터를 계산 (공격 방향의 노멀 벡터 * 임펄스 값) 해 AddImpulseAtLocation 이라는 걸로 해당 Bone 에 Impulse 를 적용한다.

ㄷㄷㄷㄷ미쳤다~~~

 

RadialDamage 인 경우

래이디얼 대미지의 원점과 범위, 힘, falloff (거리에 따른 감소), Velocity Change 여부 (이건 뭔지 몰겠음 ㅋ true 면 Field_LinearVelocity, false 면 Field_LinearImpulse 라고 하는데... 모름) 를 가지고 어떠한 물리 연산을 하고 impulse 를 적용하더라.. 여기부터는 주석도 안달려있음 ㅠ

 

어쨌든! 두근두근!

대미지를 입었다 -> 체력이 감소했다 수준보다 훨씬 무궁무진한 가능성이 열려있다는 말이다~

언젠가는 ... 화살이 날아와 체인메일 같은 갑옷의 그 작은 구멍 사이를 뚫고 심장을 꿰뚫는... 그런 계산도 가능한 걸까?