애니메이션

 

애니메이션은 AnimInstance 클래스를 만들고 블루프린트로 파생시켜서 관리하는것을 권장한다.

애니메이션을 제어할 때 데이터 접근은AnimInstance에서 하는것이 좋다. 

 

코딩 규칙

 

  • 레퍼런스로 전달된 매개변수에 값이 출력될것으로 기대된다면 Out 접두사를 붙여주는것을 권장한다.
  • const 역시 확실하게 써주는것이 좋다.

 

기타

 

생성자의 내용 변경시 라이브 코딩에 반영이 안된다. 에디터를 다시 켜주자.

이런 기능이 있나? 싶은건 웬만하면 다 구현이 되어서 레퍼런스로 존재한다.

'언리얼 엔진 > 정보' 카테고리의 다른 글

[UE5] 루멘  (0) 2022.09.29
언리얼 베테랑의 100가지 팁과 트릭 정리  (0) 2022.09.29
애니메이션 몽타주 재생 관련  (0) 2022.09.13
UMG와 관련된 내용들  (0) 2022.09.08
가비지 컬렉션  (0) 2022.09.06

[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

 

몽타주 설정을 다 했음에도 재생이 안된다면 슬롯을 살펴봐야한다.

 

----

 

의도하지는 않았지만 타이머 매니저에 콜백을 등록할 때, 시간이 궁금해서 로그를 찍어봤는데 자꾸 두번씩 찍혀서 원인을 찾아봤다.

알고보니 블루프린트나 코드상에는 문제가 없고 애니메이션 몽타주로 만들기 전에 애니메이션 애셋에다가 바로 노티파이를 집어넣었다가 이후 몽타주로 만들고 교체했는데 까먹어서 AttackEnd가 두번씩 호출되었다.

애셋에서 삭제 후 정상적인 작동을 확인했다.

 

----

 

죽음 처리를 하고 노티파이를 통해 끝부분에서 애니메이션 재생을 멈출 때, 마지막에 블렌드가 일어나는 것처럼 모션이 이상한 상태에서 굳어버린다면 노티파이를 조금 당겨주면 된다. 혹은 아무 애셋이나 하나 더 붙여서 섹션을 만들어주면 된다.

'언리얼 엔진 > 정보' 카테고리의 다른 글

언리얼 베테랑의 100가지 팁과 트릭 정리  (0) 2022.09.29
개발에 필요할 것 같은 정보들  (0) 2022.09.14
UMG와 관련된 내용들  (0) 2022.09.08
가비지 컬렉션  (0) 2022.09.06
액터의 수명 주기  (0) 2022.09.06

[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

+ Recent posts