[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

+ Recent posts