[75. Interpolating to the Enemy]

 

몬스터가 공격을 시작하고 캐릭터가 전투 범위를 벗어나지 않는다면 계속 허공에 공격을 시도하게 된다.

또한 몬스터를 바라보는 방향에 따라 공격이 빗나갈수도 있기때문에 별로 바람직한 현상은 아니다.

 

몬스터가 허공에 공격하는 것은 차후 수정하기로 하고 캐릭터가 몬스터의 전투 범위에 들어온 상태에서 공격 시, 회전보간을 적용해보자.

 

 

// Main.h
public:
    float InterpSpeed;

    bool bInterpToEnemy;

    void SetInterpToEnemy(bool Interp);

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Combat")
    class AEnemy* CombatTarget;

    FORCEINLINE void SetCombatTarget(AEnemy* Target) { CombatTarget = Target; }

    FRotator GetLookAtRotationYaw(FVector Target);
    
// Main.cpp
void AMain::Tick(float DeltaTime)
{
    // ..생략
    if (bInterpToEnemy && CombatTarget)
    {
        FRotator LookAtYaw = GetLookAtRotationYaw(CombatTarget->GetActorLocation());
        // 현재 액터의 회전자와 타겟을 바라봐야하는 회전자의 회전보간값
        FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookAtYaw, DeltaTime, InterpSpeed);

        SetActorRotation(InterpRotation);
    }
}

FRotator AMain::GetLookAtRotationYaw(FVector Target)
{
    // 액터가 Target을 바라보는 회전자를 반환
    FRotator LookAtRotation = UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), Target);
    // 그중 Yaw에 해당하는 값만 사용
    FRotator LookAtRotationYaw(0.f, LookAtRotation.Yaw, 0.f);

    return LookAtRotationYaw;
}

void AMain::SetInterpToEnemy(bool Interp)
{
    bInterpToEnemy = Interp;
}

 

// Enemy.cpp
void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        if (Main)
        {
            // 공격 범위 안에 들어오면 자기 자신을 타겟으로 알려줌
            Main->SetCombatTarget(this);
        }
    }
}

void AEnemy::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    if (OtherActor)
    {
        if (Main)
        {
            // 공격 범위를 벗어나면 초기화
            Main->SetCombatTarget(nullptr);
        }
    }
}

 

캐릭터가 일단 한번 공격을 시작하고 적의 전투범위에 들어가있다면 캐릭터는 전투범위에 있는동안 계속 몬스터를 바라보게 될 것이다.

이것은 의도한 사항이 아니다. 공격을 시도할 때만 몬스터를 바라보고 그 외에는 다른곳을 보는것이 훨씬 자연스럽다.

 

 

// Main.cpp
AMain::AMain()
{
    InterpSpeed = 15.f;
    bInterpToEnemy = false;
}

void AMain::Attack()
{
    if (!bAttacking)
    {
        bAttacking = true;
        SetInterpToEnemy(true);
        // .. 생략
}

void AMain::AttackEnd()
{
    bAttacking = false;
    SetInterpToEnemy(false); // 공격이 끝나면 보간할 적이 없다고 판단함

    if (bLMBDown)
    {
        Attack();
    }
}

 

공격 시작시 true, 종료시 false를 반복하기 때문에 지속적으로 적을 쳐다보지 않게 된다.

 

보간 전 및 전투 범위 밖에 있을 시

 

전투범위 내에서 공격시 보간을 통해 서서히 적을 바라봄

 

오토 타겟팅 게임의 경우 필요한 기능이겠지만 논타겟팅에서는 아마 필요가 없을 것이다.

논타겟팅의 경우 적의 위치보다는 마우스 커서 위치를 바라보며 회전을 해야 할 것이다.

 

[76. Enemy Attack Delay]

 

현재 몬스터의 매커니즘은 [대기 상태-어그로 범위에 있을시 추적-공격 범위에 있을시 공격] 으로 이루어져 있다.

공격 범위에 있을시 공격하는 부분은 몬스터가 지속적으로 공격하기 때문에 회피하기가 어려울 것이다.

일반적인 RPG에서의 몬스터를 보면 공격이 연속적으로 이루어지지 않고 조금씩 딜레이를 두고 다시 공격한다.

그래서 여타 게임들처럼 공격의 간격을 조정할 필요가 있다.

 

타이머 핸들을 이용하면 생각보다 쉽게 구현할 수 있을 것이다.

상수시간으로 딜레이를 줄 수도 있겠지만 랜덤한 시간으로 딜레이를 주는것도 나쁘지 않을것이다.

 

 

// Enemy.h
public:
    FTimerHandle AttackTimer; // 콜백 등록을 위한 타이머 핸들

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Combat")
    float AttackMinTime;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
    float AttackMaxTime;
    
// Enemy.cpp
void AEnemy::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            Main->SetCombatTarget(nullptr);
            bOverlappingCombatSphere = false;
            if (EnemyMovementStatus != EEnemyMovementStatus::EMS_Attacking)
            {
                MoveToTarget(Main);
                CombatTarget = nullptr;
            }
            // 공격 범위 밖으로 나갈 시, 타이머 핸들에 등록된 콜백을 모두 지운다
            GetWorldTimerManager().ClearTimer(AttackTimer);
        }
    }
}

void AEnemy::AttackEnd()
{
    bAttacking = false;

    if (bOverlappingCombatSphere)
    {
        float AttackTime = FMath::FRandRange(AttackMinTime, AttackMaxTime);
        // 공격이 끝나면 타이머 핸들에 Attack 함수를 콜백으로 등록한다
        GetWorldTimerManager().SetTimer(AttackTimer, this, &AEnemy::Attack, AttackTime);
    }
}

 

구현도 어렵지 않고 이해하는데 매우 직관적이다.

상수시간만 필요하다면 SetTimer와 ClearTimer 두개만 사용하면 될 것이다.

 

[77. Damage And Death]

 

캐릭터와 몬스터간의 피해를 입히는 부분을 제외하고는 전투의 매커니즘은 구현했다고 볼 수 있다.

언리얼에서는 데미지를 주고 받는 함수마저 미리 구현이 되어있다.

그걸 오버라이딩 해서 사용하면 된다.

 

ApplyDamage, TakeDamage 등등 종류가 다양하다.

재밌는점은 TakeDamage와 관련된 부분은 Actor, ApplyDamage와 관련된 부분은 GameplayStatics에 정의가 되어있다.

 

 

// Enemy.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
    TSubclassOf<UDamageType> DamageTypeClass;
    
// Enemy.cpp
void AEnemy::CombatOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    // .. 생략
    if (DamageTypeClass)
    {
        UGameplayStatics::ApplyDamage(Main, Damage, AIController, this, DamageTypeClass);
    }
}

 

// Main.h
public:
    virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;

// Main.cpp
float AMain::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    DecreamentHealth(Damage);

    return Damage;
}

 

Enemy_BP에서 DamageTypeClass를 DamageType으로 설정해주고 실행하면 적의 공격이 적중할 때 체력이 감소한다.

ApplyDamage를 호출했더니 플레이어의 TakeDamage가 호출되어서 감소되고 있는 것이다.

 

 

float UGameplayStatics::ApplyDamage(AActor* DamagedActor, float BaseDamage, AController* EventInstigator, AActor* DamageCauser, TSubclassOf<UDamageType> DamageTypeClass)
{
    if ( DamagedActor && (BaseDamage != 0.f) )
    {
        // make sure we have a good damage type
        TSubclassOf<UDamageType> const ValidDamageTypeClass = DamageTypeClass ? DamageTypeClass : TSubclassOf<UDamageType>(UDamageType::StaticClass());
        FDamageEvent DamageEvent(ValidDamageTypeClass);

        return DamagedActor->TakeDamage(BaseDamage, DamageEvent, EventInstigator, DamageCauser);
    }

    return 0.f;
}

 

ApplyDamage의 정의를 보면 DamagedActor, 즉 Main캐릭터의 TakeDamage를 호출하는것을 볼 수 있다.

 

체력이 다 감소되면 죽는 애니메이션까지 추가해보도록 한다.

 

// Main.cpp
void AMain::Die()
{
    UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    if (AnimInstance && CombatMontage)
    {
        AnimInstance->Montage_Play(CombatMontage, 1.f);
        AnimInstance->Montage_JumpToSection(FName("Death"), CombatMontage);
    }
}

 

과거에 이미 몽타주에 모션도 추가하고 섹션도 나눠준적이 있다. 거기에 DecreamentHP에서 Die를 실행하기 때문에 내용만 구현 해주면 우선 죽는 애니메이션이 나올 것이다.

다만 사망시 콜리전이 그대로 남아있거나 죽는 애니메이션이 딱 한번만 재생되고 다시 Idle 상태로 돌아가는 문제가 있지만 차후 수정하기로 하고 우선은 넘어간다.

 

공격을 받기만 하는것은 억울하므로 이제 몬스터도 공격을 받아야 한다.

 

 

// Enemy.h
EMS_Dead            UMETA(DisplayName="Dead"), // 열거형에 추가할 것

public:
    virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
    
    void Die();
    
// Enemy.cpp
float AEnemy::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    if (Health - Damage <= 0.f)
    {
        Health = 0.f;
        Die();
    }
    else
    {
        Health -= Damage;
    }

    return Damage;
}

void AEnemy::Die()
{
    UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    if (AnimInstance && CombatMontage)
    {
        AnimInstance->Montage_Play(CombatMontage, 1.f);
        AnimInstance->Montage_JumpToSection(FName("Death"), CombatMontage);
    }
    SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Dead);

    CombatCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    AgroSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    CombatSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);

    GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

 

Enemy에서도 똑같이 TakeDamage를 오버라이딩 하고 사망 시 모든 충돌처리가 무효가 되게 한다.

죽음 상태가 하나 추가된 것이기 때문에 얼거형에도 Dead를 추가해준다.

 

이제 진짜로 몬스터에게 피해를 입힐 차례이다.

몬스터는 무기를 따로 들고 있지 않았으므로 스스로가 공격력도 가지고 있고 직접 공격을 처리했는데 캐릭터는 조금 다르다. 무기를 휘두르긴 하지만 실제로 공격하는 객체는 무기이고 공격력도 무기가 가지고 있을 것이다.

그래서 Weapon클래스에서 ApplyDamage를 호출 할 것이다.

 

 

// Weapon.h
public:
    FORCEINLINE void SetInstigator(AController* Inst) { WeaponInstigator = Inst; }

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Combat")
	TSubclassOf<UDamageType> DamageTypeClass;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Combat")
	AController* WeaponInstigator;
    
// Weapon.cpp

void AWeapon::CombatOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    // .. 생략
    if (Enemy->HitSound)
    {
        UGameplayStatics::PlaySound2D(this, Enemy->HitSound);
    }
    if (DamageTypeClass)
    {
        UGameplayStatics::ApplyDamage(Enemy, Damage, WeaponInstigator, this, DamageTypeClass);
    }
}

void AWeapon::Equip(AMain* Char)
{
    if (Char)
    {
        SetInstigator(Char->GetController());
        // .. 생략
}

 

무기의 공격력이 25, 기본체력이 75로 되어있으므로 3번만 공격하면 죽는 애니메이션이 재생 될 것이다. 그리고 모든 콜리전을 비활성화 시켰기 때문에 더이상 충돌이 일어나지 않는다.

 

캐릭터의 죽음에서도 발생했던 문제를 해결해볼 시간이다.

죽는 애니메이션이 1회 재생되고 다시 본래의 상태로 돌아오는데, 의도하지 않은 사항이다.

 

 

이것 역시 노티파이로 해결한다.

 

// Enemy.h
public:
    UFUNCTION(BlueprintCallable)
    void DeathEnd();

    bool Alive();
    
// Enemy.cpp
AEnemy::AEnemy()
{
    EnemyMovementStatus = EEnemyMovementStatus::EMS_Idle;
}

void AEnemy::DeathEnd()
{
    GetMesh()->bPauseAnims = true; // 애니메이션 재생 일시중지
    GetMesh()->bNoSkeletonUpdate = true; // 스켈레톤 업데이트 중지
}

bool AEnemy::Alive()
{
    return GetEnemyMovementStatus() != EEnemyMovementStatus::EMS_Dead;
}

 

몬스터의 생존 여부를 판단해서 탐지범위, 공격범위 등 원하지 않는 기능에 들어가지 않도록 막아줄 안전장치도 하나 필요할 것이다.

탐지범위, 공격범위, 공격 함수를 Alive의 반환값으로 구현부를 감싸주면 된다.

 

캐릭터와 몬스터가 데미지를 주고 받고 죽음에 대한 처리까지 어느정도 마무리가 됐다.

몬스터가 사망했을 경우 오브젝트가 사라지지 않고 계속 남아있는데, 보통 게임에서는 일정시간이 지나면 사라지게끔 되어있는 경우가 많다.

 

 

// Enemy.h
public:
    void Disappear();
    
    FTimerHandle DeathTimer;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
    float DeathDelay;
    
// Enemy.cpp
AEnemy::AEnemy()
{
    DeathDelay = 3.f;
}

void AEnemy::DeathEnd()
{
    GetWorldTimerManager().SetTimer(DeathTimer, this, &AEnemy::Disappear, DeathDelay);
}

void AEnemy::Disappear()
{
    Destroy();
}

 

지금같은 경우는 따로 함수를 만들필요 없이 바로 Destory를 타이머 매니저에 등록시키면 된다.

추가적인 처리를 할 필요가 있을 경우 위와 같이 따로 만들어서 등록 시켜주면 될 것이다.

 

[정리]

 

  • 회전 보간을 통해 자연스러운 회전을 가능하게 할 수 있다. (FMath::RInterpTo)
  • 타이머 매니저를 통해 콜백을 등록하면 일정시간 뒤에 호출이 된다.
    SetTimer: 콜백등록, ClearTimer: 해제, ClearAllTimersForObject: 오브젝트에 종속된 모든 타이머 해제
  • ApplyDamage, TakeDamage를 통해 오브젝트간 피해를 주고 받을 수 있다.

 

[추가]

 

  • 노티파이가 너무 만능인것처럼 남발되는 느낌이 좀 있다. 실제로 많이 사용되는지 정보를 찾아봐야 할 것 같다.

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Combat #9  (0) 2022.09.14
[UE C++] Combat #8  (0) 2022.09.14
[UE C++] Combat #6  (0) 2022.09.13
[UE C++] Combat #5  (0) 2022.09.12
[UE C++] Combat #4  (0) 2022.09.11

[73. Combat Mechanics #3]

 

무기를 휘두르거나 타격시 소리가 재생되면 조금 더 게임같을 것이다.

공격의 주체는 캐릭터이므로 Main의 Attack에서 무기를 휘두르는 소리를 재생시켜주면 된다.

다만 무기 종류마다 소리가 다를것이므로 사운드 큐는 무기가 가지고 있는다.

 

마찬가지로 공격이 적중하는 시점은 무기에서 콜리전을 On/Off하며 관리했기 때문에 무기에서 피격음을 재생시키면 될 것이고 피격음에 대한 사운드 큐는 몬스터가 가지고 있는다.

 

// Main.cpp
void AMain::Attack()
{
    if (!bAttacking)
    {
        if (EquippedWeapon->SwingSound)
        {
            // 무기를 휘두르는것은 Main
            UGameplayStatics::PlaySound2D(this, EquippedWeapon->SwingSound);
        }
    }
}

// Weapon.h
public:
    // 휘두르는 소리는 무기가 가지고 있는다
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item | Sound")
    USoundCue* SwingSound;

// Weapon.cpp
void AWeapon::CombatOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        if (Enemy)
        {
            if (Enemy->HitParticles)
            {
                // .. 생략
                if (Enemy->HitSound)
                {
                    // 공격 적중 시점은 이 함수에서 결정된다
                    UGameplayStatics::PlaySound2D(this, Enemy->HitSound);
                }
            }
        }
    }
}

// Enemy.h
public:
    // 피격 사운드는 적이 가지고 있는다
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
    class USoundCue* HitSound;

 

어느정도 괜찮아 보이지만 무기를 휘두를때 나는 소리가 너무 일찍 재생되어서 어색하게 느껴진다.

이 부분도 노티파이를 통해서 타이밍을 조절해 주면 괜찮을 것 같다.

 

 

// Main.h
public:
    UFUNCTION(BlueprintCallable)
    void PlaySwingSound();
    
// Main.cpp
void AMain::PlaySwingSound()
{
    if (EquippedWeapon)
        UGameplayStatics::PlaySound2D(this, EquippedWeapon->SwingSound);
}

 

Main 클래스의 Attack 함수에서 소리 재생을 제거하고 노티파이로 대체한다.

 

소리 재생 타이밍이 굉장히 자연스럽게 변했다.

 

[74. Combat Mechanics #4]

 

플레이어가 공격할 때 무기를 휘두르는 소리와 타격시 소리, 이펙트가 발생했던 것처럼 몬스터가 공격하는 소리와 타격시 소리를 재생해보면 좋을 것이다.

그러면 몬스터도 노티파이를 사용해야 하므로 애니메이션 몽타주로 만들어 줄 필요가 있다.

 

몽타주를 만들어서 캐릭터와 마찬가지로 캐시 포즈와 슬롯을 이용해서 연결해준다.

 

몬스터는 캐릭터와 다르게 따로 무기를 들지 않았으므로 Enemy 클래스에 공격 판정을 위한 BoxComponent와 사운드 큐를 직접 추가해준다.

 

// Enemy.h
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
    USoundCue* SwingSound;

    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="Combat")
    class UBoxComponent* CombatCollision;

 

플레이어의 경우 무기에 BoxComponent를 입혀서 크기를 조정했지만 무기가 없는 몬스터는 어디에 붙여놔야 하는지에 대한 문제가 있다.

해결책으로 스켈레탈 메시에서 공격하는 부분에 소켓을 추가해서 해당 소켓에 BoxComponent를 붙이면 될 것이다.

 

// Enemy.cpp
AEnemy::AEnemy()
{
    CombatCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("CombatCollision"));
    CombatCollision->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetIncludingScale, FName("EnemySocket"));
}

 

 

BoxComponent가 지정한 소켓에 잘 붙었다.

 

타격시 파티클이 올바른 위치에서 나오게끔 소켓을 하나 더 추가해준다.

 

 

// Enemy.cpp
void AEnemy::BeginPlay()
{
    CombatCollision->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::CombatOnOverlapBegin);
    CombatCollision->OnComponentEndOverlap.AddDynamic(this, &AEnemy::CombatOnOverlapEnd);

    CombatCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 충돌처리 X
    CombatCollision->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic); // WorldDynamic에 대해서만 충돌판정
    CombatCollision->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore); // 모든 채널에 대한 반응을 무시함
    CombatCollision->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap); // Pawn에 대한 충돌만 겹침으로 허용
}

void AEnemy::CombatOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            if (Main->HitParticles)
            {
                const USkeletalMeshSocket* TipSocket = GetMesh()->GetSocketByName("TipSocket");
                if (TipSocket)
                {
                    FVector SocketLocation = TipSocket->GetSocketLocation(GetMesh());
                    // 월드에, 파티클을, 어느 위치에, 어느 방향으로, 재생이 끝나면 자동 파괴여부
                    UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), Main->HitParticles, SocketLocation, FRotator(0.f), false);
                }
                if (Main->HitSound)
                {
                    UGameplayStatics::PlaySound2D(this, Main->HitSound);
                }
            }
        }
    }
}

void AEnemy::CombatOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
}

 

// Main.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
    class UParticleSystem* HitParticles;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
    class USoundCue* HitSound;

 

Weapon 클래스에서 이미 구현했던 내용을 그대로 가져온거나 다름없다. Main 클래스에도 피격 파티클과 피격 사운드가 필요하기 때문에 추가해준다.

 

기존에는 적의 상태에 따라 스테이트 머신에서 Idle과 Attack간의 전환이 이루어졌지만 실제 공격 처리를 위해서는 이런 구조는 맞지 않다. 몽타주도 만들어 줬으므로 Idle과 Attack 사이의 전환규칙 및 Attack을 삭제해주고 노티파이에 의해 구현되도록 변경한다.

 

노티파이가 발생할 때마다 콜리전의 활성/비활성을 해주어야 하므로 해당 코드도 Weapon에서 작성했던 것처럼 똑같이 작성해준다.

 

// Enemy.h
public:
    UFUNCTION(BlueprintCallable)
    void ActivateCollision();
    UFUNCTION(BlueprintCallable)
    void DeActivateCollision();

// Enemy.cpp
void AEnemy::ActivateCollision()
{
    CombatCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly); // 쿼리만 처리
}

void AEnemy::DeActivateCollision()
{
    CombatCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 충돌처리 X
}

 

거의 다 왔다. 만들어둔 몽타주를 사용하고 실제 공격을 구현하자.

 

// Enemy.h
public:
    void Attack();

    UFUNCTION(BlueprintCallable)
    void AttackEnd();

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Combat")
    class UAnimMontage* CombatMontage;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Combat")
    bool bAttacking;
    
// Enemy.cpp

void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            CombatTarget = Main;
            bOverlappingCombatSphere = true;
            Attack();
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super::CombatSphereOnOverlapBegin()"));
}

void AEnemy::Attack()
{
    if (AIController)
    {
        AIController->StopMovement();
        SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Attacking);
    }
    if (!bAttacking)
    {
        bAttacking = true;
        UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
        if (AnimInstance)
        {
            AnimInstance->Montage_Play(CombatMontage, 1.35);
            AnimInstance->Montage_JumpToSection(FName("Attack"), CombatMontage);
        }
        if (SwingSound)
        {
            UGameplayStatics::PlaySound2D(this, SwingSound);
        }
    }
}

void AEnemy::AttackEnd()
{
    bAttacking = false;

    if (bOverlappingCombatSphere)
        Attack();
}

 

 

기존에 만들어뒀던 구조를 부수고 새로 만드느라 시간이 조금 걸렸지만 결국 앞에서 했던 실습들의 반복이었다.

공격 소리가 조금 빨리 들리는것 같다면 Attack 함수에서 ActiveCollision 함수로 옮기면 조금은 나을것이다.

 

[정리]

 

  • 노티파이를 이용해서 애니메이션 모션에 따라 이벤트를 발생 시킬 수 있다.

 

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Combat #8  (0) 2022.09.14
[UE C++] Combat #7  (0) 2022.09.13
[UE C++] Combat #5  (0) 2022.09.12
[UE C++] Combat #4  (0) 2022.09.11
[UE C++] Combat #3  (0) 2022.09.11

[70. Console Controller Input]

 

콘솔 컨트롤러 입력을 적용시켜 볼 것이다.

 

[프로젝트 세팅 - 입력] 에서 게임패드에 대응되는 액션이나 축을 매핑 시켜주면 된다.

 

게임패드의 경우 0과 1의 값만 대응되는 키보드 이동과는 다르게 아날로그 스틱의 조작에 따라 값이 다양하게 들어간다.

그것 때문에 아주 천천히 이동시 애니메이션이 너무 느리게 재생되어서 마치 캐릭터가 떠다니는 듯한 느낌을 받게된다.

 

 

이 문제는 블렌드 스페이스가 정교하게 설정되어있지 않아서 그렇다.

 

 

그리드 분할을 4개에서 8개로 늘리고 Walk 애니메이션을 Idle쪽에 가깝게 붙이면 이동속도가 느리더라도 걷는 모션이 충분히 재생 되어서 이제는 자연스럽게 된다.

 

별다른 작업을 하지 않아도 입력 매핑만 하면 되는 간단한 작업이다.

 

[71. Combat Mechanics #1]

 

검을 휘두를 때 적이 명중할 때를 감지할 수 있어야 하고 명중시 피해를 입혀야 한다.

기존에 있던 구체 콜리전 볼륨은 무기 습득을 위한 충돌 판정이므로 공격을 위한 새로운 콜리전 볼륨을 추가해야 한다.

 

 

// Weapon.h
public:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Item | Combat")
    class UBoxComponent* CombatCollision;

// Weapon.cpp
AWeapon::AWeapon()
{
    CombatCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("CombatCollision"));
    CombatCollision->SetupAttachment(GetRootComponent());
}

 

박스 콜리전을 만들고 적당한 크기와 위치를 조정한다.

 

Weapon은 Item을 상속받는 클래스이고 Item에 습득을 위한 OnOverlapBegin/End 함수가 바인딩 되어있다.

무기는 공격을 위한 콜리전 볼륨이 하나 더 추가되었고 해당 무기와 겹침 판정이 일어나면 공격이 이루어졌다는걸 의미하므로 또다른 OnOverlapBegin/End 함수가 필요하다.

또한 Item 클래스에서 BeginPlay 함수 내에서 바인딩 되었으므로 Weapon에서도 BeginPlay시 바인딩 해주기 위해 BeginPlay를 오버라이드 해준다.

 

 

// Weapon.h
public:
    // 상속받은 것이 아니므로 virtual과 override가 붙지 않는다
    // 차후 무기의 종류가 많아진다면 virtual정도는 붙을 수 있을 것이다
    UFUNCTION()
    void CombatOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    UFUNCTION()
    void CombatOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
protected:
	virtual void BeginPlay() override;
    
// Weapon.cpp
void AWeapon::BeginPlay()
{
    Super::BeginPlay();

    CombatCollision->OnComponentBeginOverlap.AddDynamic(this, &AWeapon::CombatOnOverlapBegin);
    CombatCollision->OnComponentEndOverlap.AddDynamic(this, &AWeapon::CombatOnOverlapEnd);
}

 

박스 콜리전에 대한 겹침 이벤트 발생에 대한 구조도 만들었으니 실제 내용을 구현할 차례이다.

 

 

내가 피해를 입는 것 외에 적이 공격을 당했을 시, 피를 뿜어내는 것과 같은 파티클 시스템이 필요하다.

적마다 다른 피격 효과를 가질 수 있으므로 파티클은 Enemy가 직접 가지도록 한다.

해당 파티클을 재생시키는 것은 Weapon에서 담당하도록 한다.

 

// Enemy.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
    float Health;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
    float MaxHealth;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
    float Damage;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
    class UParticleSystem* HitParticles;
    
// Enemy.cpp
AEnemy::AEnemy()
{
    // 지금은 안쓰지만 다음 구현을 위해 미리 추가
    Health = 75.f;
    MaxHealth = 100.f;
    Damage = 10.f;
}

 

// Weapon.h
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Item | Combat")
    float Damage;
    
// Weapon.cpp
AWeapon::AWeapon()
{
    // 지금은 안씀
    Damage = 25.f;
}

void AWeapon::CombatOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        AEnemy* Enemy = Cast<AEnemy>(OtherActor);
        if (Enemy)
        {
            if (Enemy->HitParticles)
            {
                // 월드에, 파티클을, 어느 위치에, 어느 방향으로, 재생이 끝나면 자동 파괴여부
                UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), Enemy->HitParticles, GetActorLocation(), FRotator(0.f), true);
            }
        }
    }
}

 

Enemy_BP에서 파티클을 설정 후 겹침 이벤트 발생시 파티클이 재생된다.

의도하는 사항은 공격 적중시에만 파티클이 재생되길 원하지만 지금은 공격을 하지 않아도 적과 겹치기만 하면 파티클이 재생된다.

 

우선은 여기까지만 구현하고 다음에 해당 사항을 수정하도록 한다.

 

[72. Combat Mechanics #2]

 

공격 적중시 재생되는 파티클의 위치가 정확하지 않다는 문제점이 있다.

GetActorLocation 함수를 통해 위치를 가져왔는데 이 위치가 무기의 중심점이기 때문이다. 사실상 손잡이에서 파티클이 튀는 것과 같다.

 

무기의 스켈레탈 메시에서 소켓을 추가하고 파티클이 재생되길 원하는 위치로 조절한다.

 

void AWeapon::CombatOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        AEnemy* Enemy = Cast<AEnemy>(OtherActor);
        if (Enemy)
        {
            if (Enemy->HitParticles)
            {
                const USkeletalMeshSocket* WeaponSocket = SkeletalMesh->GetSocketByName("WeaponSocket");
                if (WeaponSocket)
                {
                    FVector SocketLocation = WeaponSocket->GetSocketLocation(SkeletalMesh);
                    // 월드에, 파티클을, 어느 위치에, 어느 방향으로, 재생이 끝나면 자동 파괴여부
                    UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), Enemy->HitParticles, SocketLocation, FRotator(0.f), true);
                }
            }
        }
    }
}

 

소켓의 위치를 받아와서 해당 위치로 재생하게끔 변경하면 쉽게 해결된다.

 

이제 공격하지 않아도 상시 적용되는 공격 판정을 수정할 차례이다.

 

우선 애니메이션 몽타주에 공격 시작과 끝에 해당하는 부분에 노티파이를 설정한다.

 

// Weapon.h
public:
    UFUNCTION(BlueprintCallable)
    void ActivateCollision();
    UFUNCTION(BlueprintCallable)
    void DeActivateCollision();

// Weapon.cpp
void AWeapon::BeginPlay()
{
    CombatCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 충돌시 쿼리만(물리x)
    CombatCollision->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic); // WorldDynamic에 대해서만 충돌판정
    CombatCollision->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore); // 모든 채널에 대한 반응을 무시함
    CombatCollision->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap); // Pawn에 대한 충돌만 겹침으로 허용
}

void AWeapon::ActivateCollision()
{
    CombatCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly); // 쿼리만 처리
}

void AWeapon::DeActivateCollision()
{
    CombatCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 충돌처리 X
}

 

어떤 채널과 충돌할지는 모두 설정 되어있지만 초기에는 NoCollision 속성을 통해 충돌이 발생하지 않게 막았다.

노티파이를 통해 충돌 여부를 On/Off하는 방식으로 구현 되어있다.

 

마지막으로 MainAnim_BP에서 노티파이에 대한 처리를 해주면 공격 시 특정 구간에서만 판정이 발생하도록 변경이 가능해진다.

 

[정리]

 

  • 콘솔 컨트롤러 조작을 고려한다면 블렌드 스페이스를 잘 설정해야 한다.
  • 공격을 위한 콜리전 볼륨이 추가로 필요하다.
  • 적의 타입별로 피격 이펙트가 다를 수 있으므로 파티클만 들고있다가 공격 적중 시 무기에서 해당 파티클을 재생하도록 한다.
  • 노티파이를 통해 공격 판정을 On/Off하여 제어할 수 있다.
  • 스켈레탈 메시에 추가한 소켓의 위치로 파티클을 재생시킬 수 있다.

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Combat #7  (0) 2022.09.13
[UE C++] Combat #6  (0) 2022.09.13
[UE C++] Combat #4  (0) 2022.09.11
[UE C++] Combat #3  (0) 2022.09.11
[UE C++] Combat #2  (0) 2022.09.11

[68. Enemy Combat #3]

 

MoveTo에 전달했던 NavPath의 정보와 DrawDebugSphere를 이용해서 경로를 시각적으로 확인해 보도록 한다.

 

// Enemy.cpp
#include "Kismet/KismetSystemLibrary.h"

void AEnemy::MoveToTarget(AMain* Target)
{
    if (NavPath) // 내비게이션 메시 밖으로 나가면 크래시가 나므로 null체크를 해줘야 한다
    {
        auto PathPoints = NavPath->GetPathPoints();

        for (const auto& Point : PathPoints)
        {
            auto Location = Point.Location;
            UKismetSystemLibrary::DrawDebugSphere(this, Location, 25.f, 8, FLinearColor::Red, 10.f, .5f);
        }
    }
}

 

장애물이 없는 직선상에서는 Enemy의 원점, Target의 원점 두개가 그려지고 Target의 원점으로 쭉 이동한다.

하지만 코너같이 장애물이 있는 경우 그것을 우회하는 경로 여러개가 그려진다.

 

 

MoveRequest의 Acceptance radius에 의한 허용 반경은 캡슐 콜리전 사이의 거리와 관련이 있다.

 

[69. Enemy Combat #4]

 

지금까지는 어그로 범위 안에 들어오면 추적하는 것을 구현해 보았고 이제 전투 범위에 들어오면 공격하는 것을 구현할 차례이다.

우선은 어그로 범위에 들어오면 MoveToTarget을 호출하고 MovementStatus를 변경했던 것처럼 공격도 마찬가지로 구현하고 애니메이션 블루프린트도 동일하게 만들어준다.

 

void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Attacking);
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super::CombatSphereOnOverlapBegin()"));
}

void AEnemy::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            SetEnemyMovementStatus(EEnemyMovementStatus::EMS_MoveToTarget);
            MoveToTarget(Main); // 공격 범위에서 벗어나도 다시 추적하도록 해야한다
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super:CombatSphereOnOverlapEnd()"));
}

 

IdleWalk로 돌아오는것은 Not Equal로 해준다.

 

어그로 범위에 들어가면 추적하고 공격 범위에 들어오면 공격 모션을 취한다. 하지만 공격 모션 도중에 공격 범위를 벗어나면 애니메이션이 중단되고 다시 추적하는 문제가 있지만 잠시 뒤에 수정하도록 한다.

 

어느정도 의도한 대로 작동하고 있지만 몬스터가 영원히 쫓아오는것은 원치 않는다. 어느정도 범위를 벗어나면 몬스터가 추적을 멈추고 도망갈수 있기를 원한다.

 

 

// Enemy.cpp
void AEnemy::AgroSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Idle);
            if (AIController)
            {
                AIController->StopMovement();
            }
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super:AgroSphereOnOverlapEnd()"));
}

 

StopMovement()는 현재 무엇을 하고 있던간에 행동을 중지시킨다.

 

 

공격 도중 타겟이 범위를 벗어나면 모션이 끊기는 것을 수정해 보도록 한다.

애니메이션 노티파이를 이용해서 공격 모션이 재생되면 중단 없이 쭉 재생하고 모션의 끝에서 공격이 끝났다는 노티파이를 발생시킨 뒤 그것을 통해 다시 타겟을 추적하도록 만들 것이다.

몽타주를 따로 만들지 않았으므로 공격 애니메이션 애셋에서 노티파이를 직접 추가해주면 된다.

 

블루프린트로 처리할것이기 때문에 코드를 조금 수정하도록 한다.

 

// Enemy.h
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="AI")
    bool bOverlappingCombatSphere; // 전투 영역에 겹침 상태인지 확인하기 위함

    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "AI")
    AMain* CombatTarget; // 블루프린트에서 전투중인 타겟을 알기 위함

// Enemy.cpp

void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            CombatTarget = Main; // 전투 영역에 들어오면 타겟을 설정한다
            bOverlappingCombatSphere = true;
            SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Attacking);
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super::CombatSphereOnOverlapBegin()"));
}

void AEnemy::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            bOverlappingCombatSphere = false;
            // 아래 조건문이 실행될수가 없음
            if (EnemyMovementStatus != EEnemyMovementStatus::EMS_Attacking)
            {
                MoveToTarget(Main);
                CombatTarget = nullptr; // 전투 영역을 벗어나면 타겟을 해제한다
            }
        }
    }
    UE_LOG(LogTemp, Warning, TEXT("Super::CombatSphereOnOverlapEnd()"));
}

 

EndAttack 노티파이가 발생하면(모션이 끝나면) MoveToTarget을 호출한다.

여기서 문제점이 다시 발생한다. 타겟이 움직이지 않고 여전히 전투 영역에 있는 경우에도 EndAttack이 발생하면 전투상태가 풀리고 MoveToTarget이 무조건 호출이 되기때문에 연속적으로 공격을 하지 않는다.

 

전투 영역과 겹침 상태가 아닐때만 MoveToTarget을 호출하도록 노드의 흐름을 바꿔주면 된다.

 

 

[정리]

 

  • MoveTo에 전달되는 FNavPathSharedPtr에 경로와 관련된 정보를 돌려받는다.
  • AIController의 StopMovement 함수를 통해 행동을 중단시킬수 있다.
  • 공격 모션이 도중에 끊기는 것은 애니메이션 노티파이를 통해 해결할 수 있다. (코드만으로도 가능한지 확인 필요)

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Combat #6  (0) 2022.09.13
[UE C++] Combat #5  (0) 2022.09.12
[UE C++] Combat #3  (0) 2022.09.11
[UE C++] Combat #2  (0) 2022.09.11
[UE C++] Combat #1  (0) 2022.09.10

[66. Enemy Combat #1]

 

폰은 기능이 많지 않은 차량이나 기본적인 입력만을 받고 돌아다니는 것들에 적합하다.

하지만 캐릭터에 기능들이 좀 더 다양하므로 해당 실습에서는 폰이 아닌 캐릭터 클래스를 기반으로 적을 구현하도록 한다.

 

캐릭터를 상속받는 Enemy 클래스를 만든다.

해당 C++ 클래스를 블루프린트 클래스로 만들고 프로토타입을 작성해보도록 한다.

 

Sphere Collision을 하나 추가해서 반경을 600으로 크게 설정한다.

 

OtherActor를 Main캐릭터로 형변환 후 TargetActor로 둔다.

Acceptance Radius만큼의 Target 반경에 접근하면 이동이 완료된 것으로 간주한다.

다만 이상태로는 실행을 해도 추적하지 않는다. 추적하는 경로를 모르기 때문이다.

그래서 내비게이션 메시를 깔아주면 경로 계산이 가능해져서 추적을 하게 된다.

 

 

스케일을 50, 50, 50 정도로 설정하면 매우 넓게 설정이 된다. 캐릭터와 적이 내비게이션 메시 위에 동시에 존재하면서 겹침 이벤트가 발생 시 추적하게 된다.

 

이제 C++에서 구현할 것이기 때문에 프로토타입으로 작성한 블루프린트 이벤트는 지워주도록 한다.

 

 

 

// Enemy.h
UENUM(BlueprintType)
enum class EEnemyMovementStatus : uint8
{
    EMS_Idle			UMETA(DisplayName="Idle"),
    EMS_MoveToTarget	UMETA(DisplayName="MoveToTarget"),
    EMS_Attacking		UMETA(DisplayName="Attacking"),

    EMS_MAX				UMETA(DisplayName="DefaultMAX")
};

UCLASS()
class FIRSTPROJECT_API AEnemy : public ACharacter
{
    GENERATED_BODY()

public:
    AEnemy();

protected:
    virtual void BeginPlay() override;

public:	
    virtual void Tick(float DeltaTime) override;

    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

public:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Movement")
    EEnemyMovementStatus EnemyMovementStatus;

    FORCEINLINE void SetEnemyMovementStatus(EEnemyMovementStatus Status) { EnemyMovementStatus = Status; }
};

 

// EnemyAnimInstance.h
public:
    virtual void NativeInitializeAnimation() override;

public:
    UFUNCTION(BlueprintCallable, Category = AnimationProperties)
    void UpdateAnimationProperties();

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
    float MovementSpeed;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
    class APawn* Pawn;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Movement)
    class AEnemy* Enemy;
};

// EnemyAnimInstance.cpp
#include "Enemy.h"

void UEnemyAnimInstance::NativeInitializeAnimation()
{
    if (Pawn == nullptr)
    {
        Pawn = TryGetPawnOwner();
        if (Pawn)
        {
            Enemy = Cast<AEnemy>(Pawn);
        }
    }
}

void UEnemyAnimInstance::UpdateAnimationProperties()
{
    if (Pawn == nullptr)
    {
        Pawn = TryGetPawnOwner();
        if (Pawn)
        {
            Enemy = Cast<AEnemy>(Pawn);
        }
    }
    if (Pawn)
    {
        FVector Speed = Pawn->GetVelocity();
        FVector LateralSpeed = FVector(Speed.X, Speed.Y, 0.f);

        MovementSpeed = LateralSpeed.Size();
    }
}

 

애니메이션도 추가해 주도록 한다.

 

.

 

블렌드 스페이스에서 최대값은 150으로 설정한다.

 

[67. Ememy Combat #2]

 

블루프린트에서 프로토타입으로 확인했던 내용을 C++로 구현해볼 차례이다.

일정 범위 안에 들어오면 캐릭터를 추적하고 공격하게 할 것이다.

 

 

// Enemy.h
UENUM(BlueprintType)
enum class EEnemyMovementStatus : uint8
{
    EMS_Idle            UMETA(DisplayName="Idle"),
    EMS_MoveToTarget    UMETA(DisplayName="MoveToTarget"),
    EMS_Attacking       UMETA(DisplayName="Attacking"),

    EMS_MAX             UMETA(DisplayName="DefaultMAX")
};

UCLASS()
class FIRSTPROJECT_API AEnemy : public ACharacter
{
    GENERATED_BODY()

public:
    AEnemy();

protected:
    virtual void BeginPlay() override;

public:	
    virtual void Tick(float DeltaTime) override;

    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

public:
    // 어그로 범위, 공격 범위를 따로 구분한다.
    UFUNCTION()
    virtual void AgroSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    UFUNCTION()
    virtual void AgroSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

    UFUNCTION()
    virtual void CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    UFUNCTION()
    virtual void CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

    // 타겟으로 이동
    void MoveToTarget(class AMain* Target);

    // getter, setter
    FORCEINLINE void SetEnemyMovementStatus(EEnemyMovementStatus Status) { EnemyMovementStatus = Status; }
    FORCEINLINE EEnemyMovementStatus GetEnemyMovementStatus() { return EnemyMovementStatus; }

public:
    // 행동 상태
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Movement")
    EEnemyMovementStatus EnemyMovementStatus;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="AI")
    class USphereComponent* AgroSphere;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
    USphereComponent* CombatSphere;

    // AI를 조종하기 위한 컨트롤러
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
    class AAIController* AIController;
};

#include "Components/SphereComponent.h"
#include "AIController.h"
#include "Main.h"

// Sets default values
AEnemy::AEnemy()
{
    // Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    AgroSphere = CreateDefaultSubobject<USphereComponent>(TEXT("AgroSphere"));
    AgroSphere->SetupAttachment(GetRootComponent());
    AgroSphere->InitSphereRadius(600.f);

    CombatSphere = CreateDefaultSubobject<USphereComponent>(TEXT("CombatSphere"));
    CombatSphere->SetupAttachment(GetRootComponent());
    CombatSphere->InitSphereRadius(75.f);
}

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

    AIController = Cast<AAIController>(GetController());

    AgroSphere->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::AgroSphereOnOverlapBegin);
    AgroSphere->OnComponentEndOverlap.AddDynamic(this, &AEnemy::AgroSphereOnOverlapEnd);

    CombatSphere->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::CombatSphereOnOverlapBegin);
    CombatSphere->OnComponentEndOverlap.AddDynamic(this, &AEnemy::CombatSphereOnOverlapEnd);
}

void AEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
}

void AEnemy::AgroSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor)
    {
        AMain* Main = Cast<AMain>(OtherActor);
        if (Main)
        {
            MoveToTarget(Main);
        }
    }
}

void AEnemy::AgroSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
}

void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
}

void AEnemy::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
}

void AEnemy::MoveToTarget(AMain* Target)
{
    SetEnemyMovementStatus(EEnemyMovementStatus::EMS_MoveToTarget);

    if (AIController)
    {
        UE_LOG(LogTemp, Warning, TEXT("MoveToTarget"));
    }
}

 

AI와 관련된 모듈을 이용하려면 프로젝트의 cs파일에서 공개 의존성 모듈에 "AIModule" 을 추가해 주어야 컴파일 오류가 발생하지 않는다.

 

우선 어그로 범위 감지용 구체와 전투 범위 감지용 구체 2개를 만들어준다.

각각의 겹침 판정에 대해 함수도 바인딩 해준다.

 

우선 테스트를 위해 어그로 구체와 겹칠 시 로그를 띄우도록 했고 실행 시 잘 작동하게 된다.

 

이제 프로토타입에서 사용했던 MoveTo를 직접 코드로 작성해 본다.

 

virtual FPathFollowingRequestResult MoveTo
(
    const FAIMoveRequest & MoveRequest,
    FNavPathSharedPtr * OutPath
)

 

두개의 인자를 받는데, 첫번째는 목표에 대한 구조체이다. 접근 반경이나 길찾기 여부 등의 정보가 담겨있다. 두번째는 경로와 관련이 있는 구조체이다. OutPath라는것을 보면 알수 있듯이 경로가 OutPath에 담겨진다.

 

void AEnemy::MoveToTarget(AMain* Target)
{
    SetEnemyMovementStatus(EEnemyMovementStatus::EMS_MoveToTarget);

    if (AIController)
    {
        FAIMoveRequest MoveRequest;
        MoveRequest.SetGoalActor(Target); // 목표 액터 설정
        MoveRequest.SetAcceptanceRadius(5.f); // 수용(접근) 범위

        FNavPathSharedPtr NavPath;

        AIController->MoveTo(MoveRequest, &NavPath);
    }
}

 

애님 블루프린트에서 UpdateAnimationProperties까지 연결시켜주면 거미들이 쫓아올 때 애니메이션이 재생 된다.

 

 

[정리]

 

  • AIController를 사용하려면 cs파일의 공개 의존성 모듈에 "AIModule"을 추가하여야 한다.
  • AI가 이동하는데 사용하는 함수들은 여러종류가 있다.
    https://blog.naver.com/raveneer/220796853517

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] Combat #5  (0) 2022.09.12
[UE C++] Combat #4  (0) 2022.09.11
[UE C++] Combat #2  (0) 2022.09.11
[UE C++] Combat #1  (0) 2022.09.10
[UE C++] Gameplay Mechanics #7  (0) 2022.09.09

+ Recent posts