https://www.udemy.com/course/unreal-engine-5-the-ultimate-game-developer-course/learn/lecture/33116784#overview

 

 

맞았을 때 맞은 위치에 따라 다른 애니메이션이 출력되는 것을 구현하기 위해 수학적인 이론을 먼저 학습하자.

만약 왼쪽에서 칼로 공격을 하면 적은 오른쪽으로 가야하고 오른쪽에서 공격을 하면 왼쪽으로 가야한다.

이때 타격점과 물체 중심까지의 벡터를 만들 수 있다. 이렇게 구한 벡터와 물체의 전방을 가리키는 전방 벡터 사이의 각을 통해 어떤 방향으로 때리는 지 알 수 있다.

 

내적을 했을 때의 공식에서 만약 A와 B의 크기가 Normalize이 된다면 즉 1이 된다면 Cos값을 알 수 있다.이 값을 역 코사인 함수를 취해서 각을 알아내면 된다.

 

 이것을 코드로 구현해볼텐데 Nomalize할때 GetSafeNormal함수를 통해 계산하는 것이 안전하다.

UKismetSystemLibrary::DrawDebugArrow를 통해 각 벡터를 눈으로 볼 수 있다. 

Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint,FColor::Orange);
	PlayHitReactMontage(FName("FromLeft"));

	const FVector Forward = GetActorForwardVector();
	const FVector ToHit = (ImpactPoint - GetActorLocation()).GetSafeNormal();

	// 전방 * 방향 = |전방|*|방향| * cos(theta)
	//이때 각 크기 1이면 cos구할 수 있음
	const double CosTheta = FVector::DotProduct(Forward, ToHit);
	//역함수=> 각도 알 수 있음.
	double Theta = FMath::Acos(CosTheta);

	Theta = FMath::RadiansToDegrees(Theta);

	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + Forward * 60.f, 5.f, FColor::Red, 5.f);
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ToHit * 60.f, 5.f, FColor::Green, 5.f);
}

 

 

이렇게 해주고 중요한 점은 전방벡터의 왼쪽에 방향벡터가 있는지 오른쪽에 있는지를 판별하는 것이다. 이때 외적을 사용할 수 있다.

외적은 벡터가 동시에 직교하는 벡터를 찾아준다. 이때 외적 순서에 따라 다른 결과가 나온다. 만약 외적값이 양수라면 전방벡터를 기준으로 오른쪽에서 공격한 것이고 아니라면 왼쪽에서 공격한 것이다.이를 통해 각도의 시작 방향을 정할 수 있다.

 

이를 코드로 적용하면 다음과 같다.

Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint,FColor::Orange);
	PlayHitReactMontage(FName("FromLeft"));

	const FVector Forward = GetActorForwardVector();
	//낮은 위치에서의 타격점 - 각 벡터 지평면에 평평하게 만들기 
	const FVector ImpactLowered(ImpactPoint.X, ImpactPoint.Y, GetActorLocation().Z);
	const FVector ToHit = (ImpactLowered - GetActorLocation()).GetSafeNormal();

	// 전방 * 방향 = |전방|*|방향| * cos(theta)
	//이때 각 크기 1이면 cos구할 수 있음
	const double CosTheta = FVector::DotProduct(Forward, ToHit);
	//역함수=> 각도 알 수 있음.
	double Theta = FMath::Acos(CosTheta);

	Theta = FMath::RadiansToDegrees(Theta);

	//외적을 통해 양수면 오른쪽 음수면 왼쪽에서 공격인지 판별
	const FVector CrossProduct = FVector::CrossProduct(Forward, ToHit);
	if (CrossProduct.Z < 0)
	{
		Theta *= -1.f;

	}
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + CrossProduct * 100.f, 5.f, FColor::Blue, 5.f);

	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + Forward * 60.f, 5.f, FColor::Red, 5.f);
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ToHit * 60.f, 5.f, FColor::Green, 5.f);
}

 

 

이렇게 해주고 실제 Section을 달리해서 코드를 짜면 된다.

Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint,FColor::Orange);
	

	const FVector Forward = GetActorForwardVector();
	//낮은 위치에서의 타격점 - 각 벡터 지평면에 평평하게 만들기 
	const FVector ImpactLowered(ImpactPoint.X, ImpactPoint.Y, GetActorLocation().Z);
	const FVector ToHit = (ImpactLowered - GetActorLocation()).GetSafeNormal();

	// 전방 * 방향 = |전방|*|방향| * cos(theta)
	//이때 각 크기 1이면 cos구할 수 있음
	const double CosTheta = FVector::DotProduct(Forward, ToHit);
	//역함수=> 각도 알 수 있음.
	double Theta = FMath::Acos(CosTheta);

	Theta = FMath::RadiansToDegrees(Theta);

	//외적을 통해 양수면 오른쪽 음수면 왼쪽에서 공격인지 판별
	const FVector CrossProduct = FVector::CrossProduct(Forward, ToHit);
	if (CrossProduct.Z < 0)
	{
		Theta *= -1.f;

	}

	//디폴트 값
	FName Section("FromBack");

	if (Theta >= -45.f && Theta < 45.f)
	{
		Section = FName("FromFront");
	}
	else if (Theta >= -135.f &&  Theta < -45.f)
	{
		Section = FName("FromLeft");
	}
	else if(Theta >= 45.f &&  Theta < 135.f)
	{
		Section = FName("FromRight");
	}

	PlayHitReactMontage(Section);

	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + CrossProduct * 100.f, 5.f, FColor::Blue, 5.f);

	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + Forward * 60.f, 5.f, FColor::Red, 5.f);
	UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ToHit * 60.f, 5.f, FColor::Green, 5.f);
}

 

이렇게 해주면 잘 작동하는것을 볼 수 있다.

 

하지만 여기서 약간의 문제점이 있는데 때리는 동안의 피격점들때문에 계속 함수가 작동한다는 점이다.

이것을 한번만 작동하도록 바꿔보자

이 작업은 무기 클래스에서 OnBoxOverlap의 함수를 보면 되는데 여기서 무시할 배열들에 추가하는 것으로 중복 호출을 방지하도록 하자.

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

 

Weapon.cpp

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->GetHit(BoxHit.ImpactPoint);
		}
		IgnoreActors.AddUnique(BoxHit.GetActor());
	}
}

 

이렇게 해준 캐릭터가 무기를 장착하거나 충돌하고 난 다음 이 배열을 초기화 하도록하자.

SlashCharacter.cpp

void ASlashCharacter::SetWeaponCollisionEnabled(ECollisionEnabled::Type CollisionEnabled)
{
	if (EquippedWeapon && EquippedWeapon->GetWeaponBox())
	{
		EquippedWeapon->GetWeaponBox()->SetCollisionEnabled(CollisionEnabled);
		EquippedWeapon->IgnoreActors.Empty();
	}
}

 

이렇게 해주면 정상적으로 피격이 한번만 이루어진다.

 

 

이제 피격시에 소리가 들리도록 구현해보자

우선 피격시에 들릴 소리들을 다운해주고 이를 바탕으로 메타사운드를 만들어주자.

3개의 피격음 중에 랜덤하게 재생하는데 이대 피치와 소리의 음향도 랜덤하게 해주도록 하자.

 

이것을 적이 가지고 있다가 재생할 수 있게 코드상에서 변수로 지정해주자. 그리고 피격당했을 때 소리가 재생되도록 

UGameplayStatics의 PlaySoundAtLocation함수를 활용해주자.

Enemy.h

	private:
    UPROPERTY(EditAnywhere, Category = Sounds)
	USoundBase* HitSound;

 

Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint,FColor::Orange);
	
	DirectionalHitReact(ImpactPoint);

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

 

그리고 핫 리로드를 할때 Ctrl+Alt+F11 를 누르면 바로 가능하다.

이렇게 해주고 맵에 생성되어있는 인스턴스에 metasound를 할당해주고 실행해주면 정상적으로 재생되는 것을 볼 수 있다.

 

여기서 피격음이 거리에 따라 어떻게 다르게 들릴지 설정하기 위해 사운드 어테뉴에이션을 추가해주자.

현재 어테뉴에이션 값을 보면 구로 설정되어있는데 이 설정한 거리만큼의 반경에서 소리가 재생되고 감쇠되는 거리또한 바꿀 수 있다.

 

이것을 메타사운드에 적용할 수 도 있다.

 

 

가장 자연스럽게 소리를 구현하기 위해 어테뉴에이션을 자연으로 바꿔주었다.

이제 여기서 VFX 시각 효과를 추가해보자

언리얼에서 VFX 시각 이펙트는 Cascade와 Niagara가 있는데 나이아가라가 최신 시스템이지만 둘다 다룰줄 알아야한다.

 

우선 Cascade 시스템으로 제작된 피격 효과를 가져와서 사용해보자. 적마다 다른 피격 이펙트가 가능하도록 적 클래스에 변수를 만들어주자.

테스트를 위해 블루프린트에서는 이 이펙트를 사용하자.

Spawn Emitter의 At location과 attached가 있는데 attached는 계속 붙어있어야하는 불과 같은 이펙트에 사용하고 location은 그 장소에 이펙트가 재생되는 것이다.

 

이렇게 해주면 캐릭터 정가운데에 이펙트가 재생되는 것을 볼 수 있다.

 

이것을 코드로 구현해보자

우선 이펙트를 지정할 수 있게 변수로 만들어주고 관련 함수를 구현하자.

ctrl + k + o -> 바로 cpp파일 열수있다.

Enemy.h

	private:
    UPROPERTY(EditAnywhere, Category = VisualEffects)
	UParticleSystem* HitParticles;

Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint,FColor::Orange);
	
	DirectionalHitReact(ImpactPoint);

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

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

 

이렇게 해주고 이펙트를 지정해주면 정상적으로 작동한다.

 

여기에 공격시에 공격이펙트를 추가해보자 

이것은 공격애니메이션몽타주에서 Trail을 추가하는 것으로 할 수 있는데 Trail은 일정구간에 파티클을 실행시킬수 있는것으로 시작과 끝점을 지정해주면 된다. 이 시작과 끝점은 소켓으로 구현해두면 된다.

이를 위해 캐릭터의 스켈레톤 오른손에 소켓 2개를 추가해주자. 검의 시작과 끝에 위치시켜주면 된다.

 

이렇게 해주고 애니메이션 몽타주에 Trail을 새로운 트렉에 배치해주고 원하는 구간을 설정해주면 검기같은 이펙트가 생긴다.

+ Recent posts