[UE5]툰 탱크-19

칼든개구리·2024년 12월 12일
0

[언리얼TO리얼]

목록 보기
27/42

😵‍💫Hit Events

발사체가 어딘가 부딪치는 걸 감지해서 그 발사체를 파괴하는 방식으로 만들고 싶다.
나중에는 데미지를 입히거나 폭발 효과를 주는 것을 할 것이다.
발사체에는 메시 컴포넌트가 있다. 타입은 UStaticMeshComponent이고 UStaticMeshComponent는 언리얼 엔진 시스템 내 긴 계층 구조의 클래스 집합이고 그 부모 중 하나는 UPrimitiveComponent이다. PrimitiveComponent는 지오메트리를 포함하거나 생성하는 SceneComponents이며 주로 렌더링되거나 충돌 데이터로 사용된다. 이를 짚고 넘어가는 이유는 UStaticMeshComponent가 UPrimitiveComponent로부터 이런 기능을 상속 받기 때문이며 UPrimitiveComponent는 히트 이벤트를 생성할 수 있다.
💣히트 이벤트를 생성한다의 의미는 뭘까?
히트 이벤트는 타격을 입은 컴포넌트를 포함해 해당 충돌과 관련된 데이터를 생성한다. 부딪히는 블록은 정적 메시 구성 요소를 가지고 있어야 한다. 충돌 시 해당 컴포넌트에 접근할 수 있다.
부딪힌 액터에도 접근할 수 있고 이런 히트 이벤트는 FHitResult를 생성한다.
UStaticMeshCompont 포인터 타입 변수인 ProjectileMesh를 가지고 있다. 이 정적 메시 컴포넌트가 월드에서 어딘가 부딪히면 그에 응답하는 코드가 필요하게 된다. 정적 메시 컴포넌트는 UPrimitiveComponent 클래스를 상속받게 된다. 상속 받는 변수 중 하나는 바로 OnComponentHit이다. 이는 컴포넌트가 무언가 단단한 것에 부딪혔을 때 발생하는 이벤트라고 설명되어 있다. OnComponentHit은 FComponentHitSignature 타입이다. FComponentHitSignature타입은 구조체인 것을 알 수 있다. 또한 히트 이벤트와 오버랩 이벤트는 따로 존재하는데 OnComponentHit가 문서에는 히트 이벤트라고 되어 있다. 이 OnComponentHit이라는 히트 이벤트는 멀티태스크 델리게이트라고도 한다.
멀티캐스트 델리게이트에는 여러 개의 함수를 바인딩 할 수 있다. 여러 클래스에 존재하는 함수가 있을 수 있다. 이 함수들이 멀티캐스트 델리게이트에 바인딩되면 인보케이션 리스트라는 곳에 추가된다. 이 히트 이벤트가 호출하라고 하면 호출되는 함수 목록이다. 발사체 메시가 무언가를 치면 델리게이트에 바인딩된 함수를 가진 모든 클래스에 브로드캐스트를 보내 알려주게 된다. 브로드캐스트가 전송되자마자 모든 바인딩 함수는 브로드캐스트에 응답해 호출된다.
🧐델리게이트에 함수를 어떻게 바인딩 할까?
OnComponentHit에는 바인딩 해주는 함수가 여러 가지 있는데, 그중 하나는 AddDynamic이다.
AddDynamic를 호출할때는 사용자 객체인 this와, 콜백 함수로 우리가 만든 함수의 주소를 적어주게 되면 된다. 즉 ProjectileMesh->OnComponentHit.AddDynamic(this, &Projectile::OnHit)을 적어주게 되면 된다. OnComponentHit델리게이트에 접근해서 AddDynamic 함수를 호출하면 사용자 객체와 콜백 함수를 넘길 수 있다. 이 콜백을 히트 이벤트의 인보케이션 리스트에 추가하게 된다.

Code Time

	UFUNCTION()
	void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

콜백 함수를 만들어 주고, 이 콜백 함수는 히트 이벤트의 정확한 시그니처가 존재해야 한다. 즉 인풋 파라미터 목록이 구체적이어야 한다는 의미이다. HitComp는 부딪히는 일을 하는 컴포넌트이고 ,OtherActor은 충돌을 당하는 액터이다. OtherComp은 충돌을 당하는 컴포넌트이고, NormalImpulse가 임펄스의 방향과 크기를 나타낸다. Hit은 HitResult의 주소를 가리키는 파라미터이다. 델리게이트에 바인딩 하려면 UFUNCTION()이어야 한다.

이 후 바인딩은 BeginPlay에서 해준다. 생성자에서 하는 것은 너무 이르며 델리게이트가 바인딩 되지 않는 문제가 생길 수 있다.

void AProjectile::BeginPlay()
{
	Super::BeginPlay();
	ProjectileMesh->OnComponentHit.AddDynamic(this, &AProjectile::OnHit);
}

ProjectileMesh에서 화살표를 사용해 UPrimitiveComponent로부터 상속된 변수에 접근 가능하며 OnComponentHit을 찾고, AddDynamic을 적어준다. AddDynamci은 콜백 함수를 OnComponentHit에 바인딩 하고 해당 함수를 인보케이션 리스트에 추가하는데 사용할 함수이다.

Health Component

USceneComponent 클래스는 UActorComponent라는 클래스에서 파생된 것이다.
UActorComponent는 위치, 회전, 크기를 할 수 없고 붙이기(액터 컴포넌트를 정적 메시나 캡슐 컴포넌트에 붙이기)도 안된다. 오직 액터에 속하는 것이다. 시각적으로 드러나는 것이나 트랜스폼이 없기 때문에 어디에 붙일 필요가 없다. 이와 달리 USceneCompnent는 트랜스폼이 가능하고 다른 컴포넌트에 붙일 수 있고 다른 컴포넌트가 여기에 붙을 수 있다.
체력 관리는 트랜스폼이나 붙이는 기능은 필요 없기 때문에 UActorComponent가 적절하다.
체력 관리 c++를 만들 때 엑터 컴포넌트로 만들고 Actor은 안된다 왜냐면 엑터는 액터 컴포넌트보다 더 많은 걸 포함하는 더 큰 클래스이기 때문이다.
HealthComponent.h를 만들어 준 뒤 아래와 같이 수정해준다

private:
	UPROPERTY(EditAnywhere)
	float MaxHealth=100.f;

	float Health = 0.f; 

그리고 cpp 파일로 넘어가 BeginPlay함수를 수정해준다

Health = MaxHealth;

다음으로 언리얼 에디터로 돌아가 BP_Tank를 열어준다

맨 아래 Health 컴포넌트를 추가한다. 이제 BP_PawnTank에는 헬스 컴포넌트가 있고 탱크는 헬스 컴포넌트를 가지게 되었다. BP_PawnTank가 헬스 컴포넌트의 주인인 것이다.
이후 BP_PawnTurret에도 Health를 추가해준다.

탱크와 터랫 각각이 UHealthComponent를 가지게 되었고 발사체가 이들 중 하나와 상호작용한다면, 예를 들어 이들 중 하나와 부딪혀 히트 이벤트를 발생시킨다면, 폰은 헬스 컴포넌트에 어떤 트리거를 발생시켜 헬스 값을 변경할 것이다. 헬스 체크 기능을 컴포넌트로 만들어 그 컴포넌트에 클래스에 추가하는 건 몇가지 이점이 있다. BasePawn 클래스에 헬스 기능을 간단히 넣을 수 있고 탱크와 터렛은 상속받을 것이다. 탱크와 터렛 클래스는 작게 만들어주고 클래스마다 필요한 기능을 다룰 수 있다. 헬스 컴포넌트는 탱크와 터렛처럼 작동하지 않은 다른 액터에도 추가될 수 있다.

OnTakeAnyDamage Delegate

언리얼 엔진에는 데미지 기능도 내장되어 있으며 데미지 시스템이 매우 정교하다. 우리 게임의 모든 액터는 데미지 이벤트를 상속받는다. 이것은 OnTakeAnyDamage라고 한다. 이것은 OnComponentHit과 같이 멀티캐스트 델리게이트이다. 폰에는 UHealthComponent에 기반한 헬스 컴포넌트를 추가할 것이다. 폰이 이 OnTakeAnyDamage라는 델리게이트를 상속할 것이라는 것을 알기 때문에 헬스 컴포넌트 클래스에서 우리가 직접 함수를 만들면 우리는 이 함수를 콜백 함수로 사용할 수 있고, OnTakeAnyDamage 델리게이트에 바인딩 할 수 있다. 데미지 이벤트가 발생할 때마다 OnTakeAnyDamage 델리게이트는 브로드캐스트하고 인보케이션 리스트에 바인딩 된 함수가 호출된다.
데미지를 발생시키는 방법은 UGameplayStatics::ApplyDamage라는 함수로 가능하다.
UGameplayStatics의 정적 함수를 사용하면 ApplyDamage는 데미지를 입는 액터를 포함해 여러 파라미터를 받는다. ApplyDamage를 호출하면 액터의 OnTakeAnyDamage 델리게이트가 발사되고, 그 응답으로 콜백 함수가 호출된다. ApplyDamage는 데미지 이벤트를 생성한다.
이것을 하기 위해 3가지 단계가 필요한데, 1. 콜백 함수를 생성해야 한다. 2. 콜백 함수를 델리게이트에 바인딩한다. 3.UGameplayStatics의 ApplyDamage 함수를사용해 데미지 이벤트를 발생시킨다.

UFUNCTION()
void DamageTaken(AActor* DamagedActor, float Damage, const UDamageType* DamageType, class AController* Instigator, AActor* DamageCauser);

이 함수는 델리게이트에 바인딩 하기 때문에 해당 델리게이트에 적절한 인풋 파라미터를 줘야 한다. 첫번째로 데미지를 입은 액터인 DamagedActor, 실제 데미지 값인 Damage, DamageType이 필요한 이유는 언리얼 엔진에는 데미지 타입이라는 것이 존재한다. 공격 타입에 따라 데미지를 다르게 줄려는 의도인 것 같다. 언리얼 엔진 데미지 시스템에서 Instigator는 데미지를 관리하는 컨트롤러이다. 폰을 조종하는 플레이어 때문에 데미지가 발생했다면 Instigator는 폰을 조종하는 컨트롤러가 된다. DamageCauser은 실제로 데미지를 발생시킨 액터를 말한다,
델리게이트는 HealthComponent에 있는게 아니라 액터에 존재한다. 특히 이 컴포넌트를 소유한 폰에서 OnTakeAnyDamage 델리게이트로 연결하고 싶다
어떻게 컴포넌트 소유자에 접근해서 그 델리게이트에 연결할 수 있을까? beginplay에서 작성하고 이 컴포넌트의 소유자에 접근하는 방법은 GetOwner함수를 사용하는 것이다. GetOwner함수는 AActor 포인터를 반환한다. 이것이 컴포넌트를 소유한 액터의 포인터이다. 컴포넌트는 폰이 소유하고 폰은 액터가 소유하니까 AActor 포인터 형태로 소유자를 알려주는 것이다.

void UHealthComponent::BeginPlay()
{
	Super::BeginPlay();

	// ...
	Health = MaxHealth;
	GetOwner()->OnTakeAnyDamage.AddDynamic(this, &UHealthComponent::DamageTaken);
	
}

Applying Damage

델리게이트에 브로드캐스트하면 우리가 맞힌 물체에 데미지를 적용할 수 있다. 브로드캐스트는 ApplyDamage를 이용해서 할 것이다.

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

DamagedActor는 데미지를 입는 액터를 말한다. BaseDamage는 적용할 기본 데미지를 말하며 EventInstigator는 이 데미지를 초래한 컨트롤러를 말한다. DamageCauser은 실제로 데미지를 유발한 액터를 의미한다(폭발한 슈류탄 같은 것). DamageTypeClass은 일어난 데미지를 말하는 클래스이다. 이전에도 봤듯이 TSubclassOf는 UClass라는 인풋 파라미터 조건을 만족한다.
ApplyDamage를 호출할 때 우리가 사용할 데미지 타입을 나타내는 UClass로 넘길 수 있다.
GetOwner()은 특정 컴포넌트를 소유하는 액터를 반환하고 발사체는 동적으로 발사된다. 그래서 소유자가 지정한 곳에서 GetOwner을 호출해야 한다.

auto Projectile =GetWorld()->SpawnActor<AProjectile>(ProjectileClass, Location, Rotation);

auto는 컴파일러가 이 새로운 변수에 어떤 타입을 할당할지 알아내는 기능이다. 저렇게 선언하는 이유는 소유자를 설정하고 싶기 때문이다. 발사체를 생성한 폰으로 소유자를 지정하고 싶다.

void AProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
	auto MyOwner = GetOwner();

	if (MyOwner == nullptr) return; //컨트롤러 찾기 전에 널인지 확인 

	auto MyOwnerInstigator = MyOwner->GetInstigatorController(); //MyOwner에서 Instigator 컨트롤러를 얻을 수 있음 

	auto DamageTypeClass = UDamageType::StaticClass(); //UDamageType같은 특정 클래스 얻고 싶을 때 사용

	if (OtherActor && OtherActor != this && OtherActor != MyOwner) { //널이 아닌지, 발사체가 자신에게 데미지 주는걸 원하지 않음, 발사체가 소유자에게 데미지 주는것x
		UGameplayStatics::ApplyDamage(OtherActor, Damage, MyOwnerInstigator, this, DamageTypeClass);
		Destroy();
	}
}

TLD

델리게이트, 브로드캐스트, 인보케이션 리스트에 대해서 다시 한번 정리가 필요한 것 같다. 이번 시간에 많이 나왔지만 한번에 이해하고 적용하는데는 많은 어려움이 있는 것 같다. auto라는 것도 알게 되어 앞으로 많이 쓸 것 같다

profile
메타쏭이

0개의 댓글