Enemy(1)

groot616·2024년 4월 3일
0

언리얼5공부복습용

목록 보기
15/22

게임에는 처치해야 하는 적이 필요하다.

1. Enemy 클래스 및 블루프린트 생성

먼저 Character Type의 클래스를 하나 생성한다.
충돌 관련 설정을 먼저 진행한다.

...
#include "Components/SkeletalMeshComponent.h"
#include "Components/CapsuleComponent.h"
...
// AEnemy::AEnemy()
{
	PrimaryActorTick.bCanEverTick = true;
    // 오브젝트 충돌 타입 설정
    GetMesh()->SetCollisionObjectType(ECollicionChannel::ECC_WorldDynamic);
    // CollisionBox가 Visibility 채널에서 tracing하고 있으므로 해당 채널에 대해 block당해야 충돌 가능
    GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
    // 카메라와 충돌하면 카메라가 줌인되는 것을 방지
    GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
    // Enemy가 overlap event를 발생시키도록 함
    GetMesh()->SetGenerateOverlapEvents(true);
    // 캡슐과 충돌하면 줌인되는 카메라가 것을 방지
    GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore); 
}
...

Enemy.cpp 기반 블루프린트도 생성한다.
Enemy에 적용시킬 SK 관련 파일들을 적당히 다운로드받아 적용한다.
간혹 RootBone이 없어서 받은 에셋들을 사용하지 못하는 경우가 있으므로 그런 경우에는 RootBone을 따로 추가시켜야 한다(((Link: RootBone 적용 관련 정리))).

HitInterface를 이용한 피격반응 구현

Interface 관련 정리 링크


Enemy.h 파일을 수정한다.

// Enemy.h
...
#include "Interfaces/HitInterface.h"
...
UCLASS
class STUDY_API AEnemy : public ACharacter, public IHitInterface
{
	...
public:
	virtual void GetHit(const FVector& ImpactPoint) override;
    ...
}

이어서 Enemy.cpp 파일도 수정한다

// Enemy.cpp
...
// DrawDebug를 그리기 위해 include한 헤더파일
#include "Study/DebugMacros.h"
...
void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE(ImpactPoint);
}

Interface 클래스에 없는 parameter 를 추가하였으므로, Interface.h 에서 parameter 를 추가해준다.

// HitInterface.h
...
public:
	virtual void GetHit(const FVector& ImpactPoint) = 0;
...

마지막으로 Weapon.cpp에서 overlap 발생시 GetHit()을 호출하도록 코드를 작성한다.

// Weapon.cpp
...
#include "Interfaces/HitInterface.h"
...
void AWeapon::OnBoxOverlap(...)
{
	...
    // 실패시 null 반환하므로 이를 통해 유효값 확인
    if(CollisionBoxHit.GetActor())
    {
    	// Overlap된 Actor를 가져와서 IHitInterface로 캐스팅
        // 캐스트 성공시 해당 Actor가 인터페이스를 implement했다는 의미
    	IHitInterface* HitInterface = Cast<IHitInterface>(CollisionBoxHit.GetActor());
        if(HitInterface)
        {
        	HitInterface.GetHit(CollisionBoxHit.ImpactPoint);
        }
    }
}
...

DRAW_SPHERE 매크로 기능이 너무 한정적이므로 새로운 매크로를 하나 만들어 교체한다.

// DebugMacros.h
...
#define DRAW_SPHERE_COLOR(Location, Color) DrawDebugSphere(GetWorld(), Location, 8.f, 24, Color, false, 5.f)
...

그리고 Enemy.cpp에서 DRAW_SPHERE -> DARW_SPHERE_COLOR로 변경시키고, parameter에 FColor::Blue 를 추가하면 된다.

Enemy Animation Montage

1. HitReactMontage 생성 및 단순 재생

피격 애니메이션을 재생시키기 위해서 따로 Animation Montage 를 생성한다.
자세한 설명은 Animation Montage 쪽에 따로 서술되어 있음.
『Link: AnimationMontage』
1. SK_Paladin 기반 AnimationMontage 생성
2. 애니메이션 에셋 추가, 섹션 지정 Montage Sections 에서 Clear
3. SK_Paladin 기반 Animation Bluepirnt 생성.
4. Idle 애니메이션에서 DefaultSlot 을 경유하도록 노드를 연결.
5. BP_Enemy 에서 생성한 ABP_Paladin 애니메이션을 사용하도록 수정.
(혹여나 Animation이 재생되지 않을 경우, 4단계에서 Idle 노드의 Loop 여부 확인)


6. 코드수정(만든 캐릭터에 대부분의 그대로 활용 가능한 코드가 있으므로 참조)

  • Enemy.h 파일
// Enemy.h
...
class UAnimInstance;
...
public:
	...
protected:
	// 피격시 HitReactMontage 재생을 위한 함수
	void PlayHitReactMontage(const FName& SectionName);
private:
	// 피격시 재생시킬 Animation Montage
	UPROPERTY(EditDefaultsOnly, Category = Montage)
    UAnimInstance* HitReactMontage;
  • Enemy.cpp 파일
...
// 피격시 HitReactMontage 재생 함수
// const referense: parameter로 상수 사용 가능
void AEnemy::PlayHitReactMontage(const FName& SectionName)
{
	UAnimInstance* AnimInsatnce = GetMesh()->GetAnimInstance();
    if(AnimInstance && HitReactMontage)
    {
    	AnimInstance->Montage_Play(HitReactMontage);
        AnimInstance->Montage_JumpToSection(SectionName, HitReactMontage);
    }
}
...
// 피격시 실행될 함수
void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE_COLOR(ImpactPoint, FColor::Blue);
    PlayHitReactdMontage(FName("ReactFromLeft"));
}

7. BP_EnemyDetails 패널에서 AM_HitReact 애니메이션 몽타주 적용
8. EnableRootMotion 활성화.

2. HitReactMontage 피격 위치에 따른 재생

여러 방향으로 적이 움찍거리는 애니메이션을 적용하기 위해서는 피격 위치(vector)각도 를 구해야 한다.
2.1 위치 구하기
좌표계 중심에기준 ToHit(피격위치) 를 구하기 위해선 ImpactPoint(충돌지점) 에서 ActorLocation(Enemy의 좌표계) 를 마이너스하면 된다(벡터에서 마이너스는 정반대 방향을 의미).
그러면 위 그림과 같이 Enemy로부터 피격 지점에 대한 벡터를 구할 수 있게 된다.
2.2 각도 구하기
Enemy 기준 사분위면을 그리면 위와 같은 상태가 된다.
위 그림 기준으로 ToHit의 벡터가 135 ~ -135º 사이에 있으므로 애니메이션에서 움찔거리는 방향은 45 ~ -45º 방향이어야 하므로 ReactFromBack 애니메이션이 재생되도록 해야 한다.
그러러면 Θ를 구하여야 한다.

  • Θ 구하기
    Dot Product 를 이용하면 각도를 구할 수 있다.
AB=ABcos(θ)\vec A \cdot\vec B = \vert A \vert\enspace\vert B \vert cos(\theta)
ABAB=cos(θ)\frac{\vec A \cdot\vec B }{\vert A \vert\enspace\vert B \vert} = cos(\theta)
θ=cos1(ABAB)\theta = cos^{-1}(\frac{\vec A \cdot\vec B }{\vert A \vert\enspace\vert B \vert})

위 수식에서 unit vector 를 사용한다면 분모의 수식은 1이 되므로 결론적으로

θ=cos1(AB)\theta = cos^{-1}(\vec A \cdot\vec B)

가 된다.
코드는 아래와 같다.

// Enemy.cpp
...
// DrawDebugArrow() 사용을 위해 include
#include "Kismet/KismetSystemLibrary.h"
...
void Enemy::GetHit(const FVector& ImpactPoint)
{
	...
    // 전방 벡터
    const FVector ForwardVector = GetActor()->GetForwardVector();
    // actor에서 피격방향으로 유닛벡터
    const FVector ToHit = (ImpactPoint - GetActorLocation()).GetSafeNormal();
    // ForwardVector와 ToHit의 Dot Product 진행
    const double CosTheta = FVector::DotProduct(ForwardVector, ToHit);
    // 아크코사인으로 theta 구하기(결과값은 radian)
    double Theta = FMath::Acos(CosTheta);
    // radian에서 degree로 변환
    Theta = FMath::RadiansToDegrees(Theta);
    // 디버그용
    if(GEngine)
    {
    	GEngine->AddOnScreenDebugMessage(1, 5.f, FColor::Green, FString::Printf(TEXT("Theta: %f"), Theta));
    }
    // 전방 벡터 표시
    UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ForwardVector * 60.f, 5.f, FColor::Red, 5.f);
    // 피격 위치 표시
    UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ToHit * 60.f, 5.f, FColor::Green, 5.f);
}

실행시 Forward VectorToHit 벡터가 표시됨과 동시에 좌측 상단에 각도도 표시되는 것을 확인할 수 있다.
<!!!오류!!!>
현재 각도가 지속적으로 비슷한 값만 나오는 오류
<!!!오류!!!>


2.3 -와 + 구하기
어느 방향에서 공격하더라도 값이 양수만 나오는 문제가 발생한다. 이를 해결하기 위해서는 Cross Product 를 이용해야 한다.
언리얼에서는 오른손의 법칙이 아닌 왼손으로 적용임을 유의해야 한다.

// Enemy.cpp
...
// DrawDebugArrow() 사용을 위해 include
#include "Kismet/KismetSystemLibrary.h"
...
void Enemy::GetHit(const FVector& ImpactPoint)
{
	...
    // 언리얼에서는 왼손법칙 적용
    const FVector CrossProduct = FVector::CrossProduct(Forward, ToHit);
    if(CrossProduct.Z < 0)
    {
    	Theta *= -1.f;
    }
    UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + CrossProduct * 60.f, 5.f, FColor::Red, 5.f);
    UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ForwardVector * 60.f, 5.f, FColor::Red, 5.f);
    UKismetSystemLibrary::DrawDebugArrow(this, GetActorLocation(), GetActorLocation() + ToHit * 60.f, 5.f, FColor::Green, 5.f);
}

<!!!오류!!!>
각도및 크로스벡터 오류 발생, 임시로 랜덤값으로 몽타주 재생하도록 변경
<!!!오류!!!>

3. 피격 횟수 제한

적에게 공격시 여러번 피격되는 문제를 해결해야 한다.

  • Weapon.h
// Weapon.h
...
public:
	// 적 타격시 연속으로 피격하지 않도록 무시할 적 저장용
	TArray<AActor*> IgnoreActors;
...
  • Weapon.cpp
// Weapon.cpp
void AWeapon::OnBoxBeginOverlap(...)
{
	...
    TArrat<AActor*> ActorsToIgnore;
    ActorsToIgnore.Add(this);
    // 공격때 여러번 오버랩 되는게 문제이므로
    // 첫번째 overlap때 IgnoreActors에 추가하고
    // 두번째 overlap때 ActorsToIgnore에 추가되어서 BoxTraceSingle에 잡히지 않도록함
    for(AActor* Actor : IgnoreActors)
	{
    	ActorsToIgnore.AddUnique(Actor);
    }
	...
    if(CollisionBoxHit.GetActor())
    {
    	IHitInterface* HitInterface = Cast<IHitinterface>(CollisionBoxHit.GetActor());
        if(HitInterface)
        {
        	HitInterface->GetHit(CollisionBoxHit.ImpactPoint);
        }
        // AddUnique : 존재할 두번 추가하지 않도록 함
        IgnoreActors.AddUnique(CollisionBoxHit.GetActor());
    }
}
  • WraithCharacter.cpp
// WraithCharacter.cpp
...
void AWraithCharacter::SetWeaponCollision(ECollisionEnabled::Type CollisionEnabled)
{
	if(EquippedWeapon && EquippedWeapon->GetWeaponBox())
    {
    	EquippedWeapon->GetWeaponBox()->SetCollisionEnabled(CollisionEnabled);
        EquippedWeapon->IgnoreActors.Empty();
    }
}
...
  1. IgnoreActor를 만들고 첫 overlap시 피격actor를 IgnoreActor에 저장
  2. 두번재 overlap이 시작되면 IgnoreActor에 있는 피격 actor를 ActorsToIgnore로 옮겨 저장하게됨. OnBoxBeginOverlap의 parameter에 들어가므로 다음 overlap 안됨.
  3. 캐릭터 무기의 충돌가능시점과 충돌불가능시점 notify에서 SetWeaponCollision()를 실행하고, 해당 함수에서 IgnoreActors를 비움으로써 충돌 가능시점과 충돌 불가능 시점에 IgnoreActor를 비움.
    (13.137)

피격 사운드 재생

피격시 재생시킬 메타사운드를 제작한다.
먼저 필요한 사운드 에셋을 다운로드하여 적절한 위치에 import 한다.
SFX_HitFlesh 메타사운드를 하나 생성하고, 변수를 추가한뒤 타입을 WaveAsset , Array 체크 활성화를 통해 전부 추가한다.
필요에 따라 피치와 노드를 랜덤하게 조절한다.
그리고 코드를 작성한다.

  • Enemy.h
// Enemy.h
...
private:
	// 피격시 피격사운드
    UPROPERTY(EditAnywhere, Category = "Sound")
	USoundBase* HitSound;
...
  • Enemy.cpp
// Enemy.cpp
...
#include "Kismet/GameplayStatics.h"
...
void AEnemy::GetHit(const FVector& ImpactPoint)
{
	...
    if(HitSound)
    {
    	UGameplayStatics::PlaySoundAtLocation(this, HitSound, ImpactPoint);
    }
}
...

마지막으로 BP_Enemy에 sound를 추가해주면 적용완료.

SoundPlayAtLocation인데 거리가 멀어져도 사운드크기가 같은 문제

우클릭후 Sound Attenuation 생성.

  • Attenuation Function
    Linear하게 감쇠, 다른 설정시 거기에 맞게 감쇠
  • Attenuation Shape
    구 형태로 감쇠
  • Inner Radius
    Attenuation Shape 가 구이므로 구 형태로 감쇠한다고 가정하고 400unit 내에서 풀사운드로 들림
  • Falloff Distance
    Attenuation Shape 가 구이므로 구 형태로 감쇠한다고 가정하고 3600unit 밖에선 아무 소리도 안들림

ATtenuation FunctionNatural Sound 로 변경한다.
생성해둔 MetaSound를 다시 열고, Source 를 누르면 Attenuation 추가가 가능하다.
제대로 적용되었는지 확인하기 위해 BP_Enemy 에 위의 노드들을 구성하고 컴파일 후 실행하면 거리에 따라 소리가 조절되는 것을 확인할 수 있다.

VFX(Visual Effects)

VFX의 종류는 Cascade, Niagara 두가지가 있다.

1. HitParticles

Cascade(Blueprint)

먼저 출혈 이펙트 에셋을 import 한다.
출혈 이펙트가 출력되는 것을 확인하기 SpawnEmitterAtLocation 노드를 사용하고, 출력이 정상적으로 작동하는지 확인하기 위해 위에서 사용한 Delay 노드를 사용한다.

  • Emitter Template
    Cascade Lagacy System, 사용할 파티클 에셋 선택
  • Locaion
    particle 재생 위치 정보
  • Rotation
    particle 회전 정보
  • Scale
    particle 크기 정보
  • Auto Destroy
    particle 재생 후 바로 파괴(메모리 관련 최적화 가능)
  • Pooling Method
    풀링 관련
  • Auto Activate System
    particle 작동을 월드에서 볼 수 있는지에 대한 bool값
    실행시 출혈 이펙트가 매 딜레이마다 재생됨을 확인할 수 있다.

Cascade(c++)

우선 EventBeginPlay 에서 노드 연결을 끊어준 뒤 코드를 작성한다.

  • Enemy.h
...
private:
	// 피격시 출력될 particle
	UPROPERTY(EditAnywhere, Category = "VisualEffects")
    UParticleSystem* HitParticles;
...
  • Enemy.cpp
...
void AEnemy::GetHit(const FVector& ImpactPoint)
{
	...
    if(HitParticles)
    {
    	UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), HitParticles, ImpactPoint);
    }
}

마지막으로 BP_Enemy 에서 HitParticles 를 설정해주면 된다.

2. Weapon Trails

무기를 휘두르면 궤적을 따라 이펙트가 발생한다.
먼저 AM_AttackMontage 에 궤적을 그릴 트랙을 추가해준다.
해당 트랙에 Add Notify States -> Trail 을 선택한다.
Notify의 길이를 정해줄 수 있는데, 이걸로 궤적이 나타나는 구간을 정할 수 있다.
우선 적당히 길이를 정하고, 우측의 Details 패널에서 PSTemplate 에 에셋을 적용시킨다.
Trail 을 적용시키기 위해서 Trail 의 시작지점 Socket과 끝지점 Socket을 추가해주어야 한다.

Socket 이름을 입력해준다.
제대로 Trail Effect가 적용되어 나타나는 것을 확인할 수 있다.

0개의 댓글