언리얼 엔진 5/개발 일지

[UE5] 블루아카이브 TPS게임 개발일지 (45) - 피격 방향 표시기 구현 2

ciel45 2024. 1. 20. 13:07

이제 이 표시기가 피격 시 화면에 떠야 한다.

 

맞을 때마다 일일히 화면에 띄워주고, 없애주기 귀찮아 약간의 꼼수를 썼다.

 

사실은 항상 화면에 떠있는 채로 투명도를 0으로 하고, 피격당했을 시에만 투명도를 1로 바꿔주는 것이다.

 

우선 KannaCharacter의 헤더에 다음과 같은 변수를 추가하였다.

	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
	TSubclassOf<UDamageIndicator> DamageIndicatorClass;

	UPROPERTY(BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
	UDamageIndicator* DamageIndicator;

 

 

TSubclassOf<UDamageIndicator>는 말그대로 UDamageIndicator의 서브클래스 타입을 담을 수 있다.

탄약 UI를 만들었을 때 처럼, 블루프린트 클래스를 C++에서 사용하기 위해서 만든 것이다.

https://ciel45.tistory.com/55

 

블루아카이브 TPS게임 개발일지 (38) - 탄약 UI 구현 1

슬슬 탄약 시스템과 재장전 시스템을 만들 때가 된 것 같다. 장전 로직의 핵심을 정리해보자면 다음과 같다. 총을 발사할 때마다 탄약 개수가 1씩 줄어든다. 현재 탄약이 0이면 발사하지 못한다.

ciel45.tistory.com

이렇게 함으로써 블루프린트 클래스를 C++에서 참조할 수 있고, DamageIndicator는 그 클래스를 통해 만들어낸 인스턴스를 담을 변수이다.

 

 

 

DamageIndicator를 만들고, 처음 화면에 띄워주는 작업은 KannaTPSHUD를 초기화할 때 같이 수행하도록 해주었다.

void AKannaCharacter::InitKannaTpsOverlay()
{
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		AKannaTPSHUD* KannaTPSHUD = Cast<AKannaTPSHUD>(PlayerController->GetHUD());
		if (KannaTPSHUD)
		{
			KannaTPSOverlay = KannaTPSHUD->GetKannaTPSOverlay();
			if (KannaTPSOverlay)
			{
				KannaTPSOverlay->HideAmmoText();
			}
		}
		DamageIndicator = CreateWidget<UDamageIndicator>(PlayerController, DamageIndicatorClass);
		DamageIndicator->AddToViewport();
		DamageIndicator->SetRenderOpacity(0.f);
	}
}

SetRenderOpacity를 통해 초기 투명도를 0으로 설정해준 것을 볼 수 있다.

 

위젯이 나타나는 것은 KannaCharacter의 TakeDamage()에서 구현할 것이다.

그런데, TakeDamage는 ApplyDamage()에 의해 호출되어야 할 것이다.

그 ApplyDamage()를 호출하는 클래스는 Projectile.cpp이다.

 

그 부분은 다음과 같다.

void AProjectile::Damage(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
	//맞은 액터가 칸나였을 경우
	if (Cast<AKannaCharacter>(OtherActor))
	{
		UE_LOG(LogTemp, Warning, TEXT("플레이어 피격!"));
		UGameplayStatics::ApplyDamage(OtherActor, 20.f, GetInstigator()->GetController(), this, UDamageType::StaticClass());
	}

	Destroy();
}

OtherActor를 AKannaCharacter로 캐스팅을 시도하는데,

만약 칸나가 아니었다면 결과가 null이 되어 자동으로 if문이 실패하고,

칸나가 맞았다면 캐스팅이 성공하여 if문이 실행된다.

 

 

그렇다면 이제 KannaCharacter의 TakeDamage를 수정할 차례이다.

float AKannaCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)													
{
	/* 생략 */
	if (DamageIndicator)
	{
		if (DamageIndicator->GetRenderOpacity() == 0.f)
		{
			DamageIndicator->SetRenderOpacity(1.f);

			DamageIndicator->Causer = EventInstigator->GetPawn();

			FLatentActionInfo Info;
			Info.CallbackTarget = this;
			Info.Linkage = 0;
			Info.ExecutionFunction = FName("FadeOutDamageIndicator");
			Info.UUID = 123;
			UKismetSystemLibrary::RetriggerableDelay(GetWorld(), 3.f, Info);
		}
		else
		{
			DamageIndicator->Causer = EventInstigator->GetPawn();
		}
	}

	/*생략*/
}

공격을 받았을 때, DamageIndicator의 투명도가 0이었다면 1로 바꿔 화면에 보이게 하고, Causer를 EventInstigator, 즉 총을 쏜 적으로 설정한다.

 

FLatentActionInfo 구조체를 사용했는데, 타이머와 비슷하지만 조금 다르다.

 

타이머는 그냥 일정시간 기다린 뒤 함수를 호출하는 것이 다이지만, FLatentActionInfo는 일정시간 기다리다가 또 뭔가 조건이 발생하면 처음부터 초를 다시 센다.

 

피격방향 표시기를 키고 끌 때까지 3,2,.. 세다가 플레이어가 한번 더 피격을 당하면 다시 3, 2, ... 이렇게 세는 것이다.

 

자세한 내용은

https://docs.unrealengine.com/4.26/en-US/API/Runtime/Engine/Engine/FLatentActionInfo/

 

FLatentActionInfo

Latent action info.

docs.unrealengine.com

 

CallbackTarget은 타이머 후 호출할 함수가 들어있는 오브젝트이므로 this로 해주면 되고, ExecutionFunction에는 새로 만든 함수인 FadeOutDamageIndicator를 이름으로 달아주었다.

 

Linkage는 함수의 재개 포인트를 의미한다는데, 재개하지 않고 처음부터 할 것이므로 0으로 해주고, UUID도 현재 ID가 상관있는건 아니므로 아무 값인 123으로 넣어주었다.

 

이제 플레이어가 피격을 받고 3초동안 안맞고 잘 숨어있으면, UI가 천천히 사라진다.

 

FadeOutDamageIndicator함수는 다음과 같다.

void AKannaCharacter::FadeOutDamageIndicator()
{
	DamageIndicator->PlayFadeAnim();
}

DamageIndicator의 PlayFadeAnim을 호출하는 것 뿐이다.

 

한가지 주의할 점은, FLatentActionInfo에 달아주는 함수는 UFUNCTION()이 붙어있어야 한다.

 

 

다음 포스팅에서는 Projectile의 Instigator, 즉 총알을 쏜 주인을 설정해주는 것, 그리고 위젯을 회전시키는 것에 대해 다룰 것이다.