https://www.udemy.com/share/107vg03@Xgb5iRXcpg26gJ-bUYComus3EEfNr2SumFm1Wlohh2QkeHxGAaMbERJi-mc4LTXQig==/

 


이제 체력바는 있기 때문에 이를 활용해보자

적에게 Damage를 주면 체력바의 퍼센트가 감소하도록 하자.

이때 AActor클래스에 있는 TakeDamage함수를 재정의하여 사용하자. 이 함수를 통해 데미지를 입었을 때 발생할 함수를 연결해줄 수 있다.

 

 

그리고 ApplyDamage를 통해 데미지를 줄 수 있다. 이때 EventInstigator은 데미지를 주는 객체를 소유하고 있는 객인데 이때 칼을 쥐고 있는 캐릭터가 여기에 해당된다. DamageCauser은 칼 그자체를 뜻한다.

 

 

전체적인 작동방식은 다음과 같다

 

이제 실제 코드에서 적용해보자. 우선 무기에서 ApplyDamage를 구현하자. 우선 장착할때 Owner와 Instigator를 설정하도록 변경해주자.

Weapon.cpp

void AWeapon::Equip(USceneComponent* Inparent, FName InSocketName, AActor* NewOwner, APawn* NewInstigator)
{
	SetOwner(NewOwner);
	SetInstigator(NewInstigator);
	AttachMeshToSocket(Inparent, InSocketName);
	ItemState = EItemState::EIS_Equipped;
	if (EquipSound)
	{
		UGameplayStatics::PlaySoundAtLocation(
			this,
			EquipSound,
			GetActorLocation()
		);
	}
	if (Sphere)
	{
		Sphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	}
	if (EmbersEffect)
	{
		EmbersEffect->Deactivate();
	}
}

void AWeapon::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	const FVector Start = BoxTraceStart->GetComponentLocation();
	const FVector End = BoxTraceEnd->GetComponentLocation();

	TArray<AActor*> ActorsToIgnore;
	ActorsToIgnore.Add(this);

	for (AActor* Actor : IgnoreActors)
	{
		ActorsToIgnore.AddUnique(Actor);
	}

	//충돌 결과
	FHitResult BoxHit;

	UKismetSystemLibrary::BoxTraceSingle(
		this,
		Start,
		End,
		FVector(5.f, 5.f, 5.f),
		BoxTraceStart->GetComponentRotation(),
		ETraceTypeQuery::TraceTypeQuery1,
		false,
		ActorsToIgnore,		//무시할거
		EDrawDebugTrace::ForDuration,
		BoxHit,
		true		//자신무시
	);

	if (BoxHit.GetActor())
	{
		IHitInterface* HitInterface = Cast<IHitInterface>(BoxHit.GetActor());
		if (HitInterface)
		{
			HitInterface->Execute_GetHit(BoxHit.GetActor(), BoxHit.ImpactPoint);
		}
		IgnoreActors.AddUnique(BoxHit.GetActor());

		CreateFields(BoxHit.ImpactPoint);

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

 

이렇게 해주고 Enemy 클래스에서 TakeDamae를 override해서 사용해주면 된다.

Enemy.cpp

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

	return DamageAmount;
}

 

이렇게 해서 테스트해보면 정상적으로 작동하는 것을 볼 수 있다.

 

여기서 체력바를 조금 커스텀해보자. 이를 위해 커스텀 체력바를 다운받아주고 각 이미지을 프로젝트에 import해준 다음 압축 설정을 UserInterface2D로 설정해주자

 

이렇게 해준다음 배경이미지와 필이미지를 커스텀 이미지로 변경해주는데 이때 기존에 색이 영향을 주지않도록 Fill Color의 RGBA값을 모두 1로 설정해주자

그리고 배경의 색이 투명하도록 배경의 색조에 A값을 0으로 설정해주자

그리고 이미지를 활용하여 윤곽선을 나타내도록 해주자

 

이렇게 해주면 커스텀 HP바가 잘 적용된 것을 볼 수 있다.

 

이제 적의 체력이 0이 됐을 때 Death 애니메이션이 출력되도록 만들어보자.

우선 Death 애니메이션을 Mixamo에서 다운받아주자. 이때 전에 해줬던 것처럼 Root본을 추가해주는 작업을 Blender의 플러그인을 통해 해주면 된다. 애니메이션은 Death 애니메이션 중에 마음에 드는 것을 4개정도 다운받아주자.

다운받아준 다음 Blender를 열고 mixamo 사이드탭을 열어준다. 설정값은 다음과 같다. Input Path에 다운받은 폴더를 넣어주고 Output Path는 Convert된 값이 들어갈 곳으로 원하는 폴더를 지정해주면 된다. 설정해준 다음 Convert해주자.

이렇게 변환된 것을 이제 임폴트 해주면 되는데 이때 Skeleton은 SK_Paladin으로 지정해주고 매시 임포트는 꺼주자.

이렇게 해준다음 애니메이션 몽타주를 만들어주자. 이때 각 몽타주 섹션이 독립적이도록 섹션을 만든다음 지우기를 해주어야한다.

 

이제 체력이 0이 될 때 이 몽타주중 하나를 재생하게 해주면된다. 이를 위해 우선 체력이 0보다 큰지 확인해줄 bool 함수를 만들어주자.

AttributeComponent.cpp

bool UAttributeComponent::IsAlive()
{
	return Health > 0.f;
}

 

이렇게 해준 다음 적이 죽었는지 GetHit에서 확인해서 그에 따라 다른 몽타주를 재생하도록 무기에서 충돌할 때 데미지를 먼저 준 다음 GetHit이 실행되도록 순서를 바꿔주자.

Weapon.cpp

if (BoxHit.GetActor())
{
	UGameplayStatics::ApplyDamage(
		BoxHit.GetActor(),
		Damage,
		GetInstigator()->GetController(),
		this,
		UDamageType::StaticClass()
	);

	IHitInterface* HitInterface = Cast<IHitInterface>(BoxHit.GetActor());
	if (HitInterface)
	{
		HitInterface->Execute_GetHit(BoxHit.GetActor(), BoxHit.ImpactPoint);
	}
	IgnoreActors.AddUnique(BoxHit.GetActor());

	CreateFields(BoxHit.ImpactPoint);

	
}

 

이렇게 해준 다음 GetHit에서 살았는지 확인하고 살았다면 HitReact를 죽었다면 Die함수를 실행하도록 하자.

Enemy.cpp

void AEnemy::Die()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && DeathMontage)
	{
		AnimInstance->Montage_Play(DeathMontage);
		const int32 Selection = FMath::RandRange(1, 6);
		FString SectionString = FString::Printf(TEXT("Death%d"), Selection); // "Death1", "Death2" 등의 문자열 생성
		FName SectionName = FName(*SectionString); // FString을 FName으로 변환
		AnimInstance->Montage_JumpToSection(SectionName, DeathMontage);
	}
}

void AEnemy::GetHit_Implementation(const FVector& ImpactPoint)
{
	if (Attributes && Attributes->IsAlive())
	{
		DirectionalHitReact(ImpactPoint);
	}
	else
	{
		Die();
	}
	
	

	if (HitSound)
	{
		UGameplayStatics::PlaySoundAtLocation(
			this,
			HitSound,
			ImpactPoint
		);
	}

	if (HitParticles&& GetWorld())
	{
		UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld(),
			HitParticles,
			ImpactPoint
		);
	}
}

 

이렇게 해주면 Die 몽타주의 섹션이 재생되지만 바로 Idle로 돌아오기 때문에 계속 누워있을수 있도록 수정해주어야 한다.

이를 위해 각 애니메이션에서 제일 마지막부분을 따로 애니메이션으로 만들기 위해 마지막 부분을 선택한 뒤 에셋생성/애니메이션 생성/현재포즈를 통해 만들어주자.

그 다음 애니메이 블루플린트에서 새로운 State Machine을 만들어주자.

 

이 Main States에는 Idle과 Dead가 있고 내부 에는 각각의 애니메이션이 재생되도록 한다. 

 

이때 Idle과 Dead의 전환 조건도 만들어줘야하고 어떤 Dead애니메이션이 재생될지도 정해줘야한다. 이를 코드로 구현배자 Death포즈 여러개와 Alive 포즈를 정의해줄 enum 클래스를 선언해주자.

UENUM(BlueprintType)
enum class EDeathPose :uint8
{
	EDP_Alive UMETA(DisplayName = "Alive"),
	EDP_Death1 UMETA(DisplayName = "Death1"),
	EDP_Death2 UMETA(DisplayName = "Death2"),
	EDP_Death3 UMETA(DisplayName = "Death3"),
	EDP_Death4 UMETA(DisplayName = "Death4"),
	EDP_Death5 UMETA(DisplayName = "Death5"),
	EDP_Death6 UMETA(DisplayName = "Death6")
};

 

이렇게 정의해준 enum 클래스를 Enemy 클래스에서 사용하도록 하자. 이때 애니메이션 블루프린트에서 사용할 수 있게 UPROPERTY값을 BluePrintReadOnly로 설정해주었다.

Enemy.h

protected:
	UPROPERTY(BlueprintReadOnly)
	EDeathPose DeathPose = EDeathPose::EDP_Alive;

 

그리고 Die함수 내에서 각 Death 애니메이션 섹션에 맞게 enum으로 선언된 변수도 바뀌도록 static_cast를 통해 처리해주었다.

Enemy.cpp

void AEnemy::Die()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && DeathMontage)
	{
		AnimInstance->Montage_Play(DeathMontage);
		const int32 Selection = FMath::RandRange(1, 6);
		FString SectionString = FString::Printf(TEXT("Death%d"), Selection); // "Death1", "Death2" 등의 문자열 생성
		DeathPose = static_cast<EDeathPose>(RandomIndex);
		FName SectionName = FName(*SectionString); // FString을 FName으로 변환
		AnimInstance->Montage_JumpToSection(SectionName, DeathMontage);
	}
}

 

이렇게 해주고 이 Enum 변수값을 가져오기 위해 애니메이션이 Initialize 될때 Enemy값을 변수로 가져올 수 있도록 해주자.

 

이때 DeathPose의 값을 가져오는 것을 멀티스레딩를 활용하여 가져오자. 이것은 Tread Safe Update Animation을 활용하여 만들 수 있는데 이때 멀티 스레딩에서 값에 안전하게 접근하도록 Property Access를 활용한다.

 

이렇게 해주면 이제 Death Pose가 Alive가 아니라면 Death State로 넘어가면 되게 해주면 된다.

 

이렇게 해준 다음 Dead에서는 DeadPose 변수값을 가져와서 Blend Pose를 통해 각 애니메이션에 맞는 Dead 애니메이션을 마지막에 출력해주도록 하자

 

 

이렇게 해주고 블랜드 타임을 0으로 해주면 잘 작동하는 것을 볼 수 있다. 

+ Recent posts