UE4

[UE4] 데미지 프레임워크

Honey Badger 2022. 10. 21. 21:03

TakeDamage()  

   언리얼 엔진의 액터 클래스 AActor에는 TakeDamage라는 함수가 구현돼 있다. 이 함수를 사용하면 손쉽게 액터에 데미지를 전달할 수 있다. TakeDamage 함수는 총 네 개의 인자를 가지고 있다.

  - DamageAmount : 전달할 데미지의 세기

  - DamageEvent : 데미지 종류

  - EventInstigator : 공격 명령을 내린 가해자

  - DamageCauser : 데미지 전달을 위해 사용한 도구

 

여기서 데미지를 가한 진정한 가해자는 폰이 아니라 폰에게 명령을 내린 플레이어 컨트롤러라고 할 수 있으므로 EventInstigator에는 폰정보가 아닌 플레이어 컨트롤러의 정보를 보내줘야 한다. 

if (bResult)//충돌이 감지되면
	{
		if (HitResult.Actor.IsValid())
		{
			UE_LOG(LogTemp, Error, TEXT("Hit Actor Name : %s"), *HitResult.Actor->GetName());
			FDamageEvent DamageEvent;
			HitResult.Actor->TakeDamage(50.0f, DamageEvent, GetController(), this);//AActor에서 제공하는 함수. (전달할 데미지 세기, 데미지종류, 가해자, 직접피해를입힌 Actor)
		}
	}

모든 액터의 설정에는 Can be Damaged라는 속성이 있다. 이 속성의 체크를 해제하면 캐릭터에 전달된 데미지의 결과가 모두 0이 되는 무적 상태로 변한다. 

 

CharacterStatComponent와 연동하기

  캐릭터가 데미지를 받으면 받은 데미지만큼 CurrentHP에서 차감하고, 그 값이 0보다 작거나 같으면 캐릭터가 죽도록 해보자. TakeDamage 함수에서 직접 처리해도 되지만 CharacterStatComponent에 SetDamage함수를 생성하고 캐릭터의 TakeDamage함수에서 이를 호출해 데미지를 ActorComponent가 처리하도록 한다.

   ActorComponent는 데미지 계산을 처리하되 CurrentHP값이 모두 소진되면 죽었다고 캐릭터에게 알려줘야 한다. 물론 캐릭터를 Stat코드에 불러와 Die함수를 호출해주어도 되지만 코드가 서로 의존성을 갖지 않도록, ActorComponent에 델리게이트를 선언하고 캐릭터에서 이를 바인딩시키는 형태로 구조를 설계해보자. 

 

먼저 CharacterStatComponent의 헤더에 델리게이트를 선언한다.

DECLARE_MULTICAST_DELEGATE(FOnHpIsZeroDelegate); //델리게이트 매크로 선언

 

그 다음 데미지에 따라 스텟의 HP를 재설정하는 SetDamage함수를 정의한다. 

void UMyCharacterStatComponent::SetDamage(float NewDamage)
{
	if (nullptr != CurrentStatData)
	{
		CurrentHP = FMath::Clamp<float>(CurrentHP - NewDamage, 0.0f, CurrentStatData->MaxHP);
		if (CurrentHP <= 0.0f)
		{
			OnHPIsZero.Broadcast(); //OnHPIsZero에 바인딩되어 있는 함수들 호출.
		}
	}	
}

 

스텟중 공격력을 리턴하는 GetAttackPower함수도 정의한다. 

float UMyCharacterStatComponent::GetAttackPower()
{
	if (nullptr == CurrentStatData)
		UE_LOG(LogTemp, Error, TEXT("CurrentStatData is null."));
	return CurrentStatData->AttackPower;
}

 

캐릭터의 PostInitializeComponents함수에서 캐릭터의 체력이 Zero가 되었을때 죽는 모션과 콜리전관리를 해줄 람다함수를 정의해 OnHPIsZero 델리게이트에 바인딩시켜준다. 

CharacterStat->OnHPIsZero.AddLambda([this]() ->void {
	MyAnim->SetDeadAnim();
	SetActorEnableCollision(false);
	});

 

마지막으로 override된 함수인 TakeDamage에 CharacterStat->SetDamage(FinalDamage)로 데미지를 입도록 정의하고, AttackCheck함수의 데미지 설정부분에 CharacterStat->GetAttackPower()으로 캐릭터의 공격을 받은 액터가 캐릭터의 AttackPower만큼의 데미지를 입게 하면 완성이다.  

 

 

전체적인 코드흐름

  복습할겸 데미지 흐름을 정리해보자. 쉽게 이해를 돕기위해 때린사람을 A, 맞은대상을 B라고 하겠다.

 

1. A의 MyCharacter클래스에 정의되어있는 AttackCheck()에서 피격이 감지된 B의 TakeDamage()가 호출된다.

2. B의 TakeDamage()에선 B의 StatComponent에 속해있는 SetDamage()가 호출된다.

3. B의 SetDamage()에선 같은 클래스 내의 SetHP()를 호출한다. 

4. SetHP()에선 OnHPChanged델리게이트가 BroadCast되고, 만약 Set할 HP가 0이라면 OnHPIsZero도 BroadCast된다.

5. B의 MyCharacterWidget 클래스에 있는 BindCharacterStat()에서 OnHPChanged델리게이트에 바인딩되어있는 UpdateHPWidget()가 호출된다.

6. B에 연동되어있는 ProgressBar에 SetPercent()로 스탯의 HP비율값을 받아 적용시켜준다.

 

 

    단순해보이는 로직이 이렇게나 복잡하게 연동되어있다는 사실이 정리하고 보니 더 놀랍다.. 유니티 하던 시절엔 그냥 이런 구조적인 코딩을 해볼 생각도 없었고 기능구현에만 신나있었던 기억이 난다. 언리얼을 하면서 여러모로 고생도 많이 했지만 그만큼 실력이 눈에띄게 늘은 것 같아서 다행이다!  하지만 절대 자만하지 말아야 한다. 배움에는 끝이 없다는 말을 마음에 되새기고 항상 노력하는 사람이 되자..