Post

언리얼 밀리 공격 구현

AnimNotify + Interface 조합으로 근접 공격 구현

언리얼 밀리 공격 구현

언리얼 밀리 공격 구현


공격 흐름

근접 공격은 아래 파이프라인으로 동작한다.

1
2
3
4
5
6
7
입력
  └→ ComboAttack()        공격 플래그 설정 + 몽타주 재생
      └→ AnimNotify       몽타주의 특정 프레임에서 발동
          └→ DoAttackTrace()      구체 스윕으로 히트 감지
              └→ ApplyDamage()    ICombatDamageable 인터페이스로 호출
                  └→ TakeDamage() HP 차감
                      └→ HandleDeath()  HP <= 0

입력 처리


Move/Look은 기본 ThirdPerson을 상속받고, 공격 관련 입력(Combo, Charge)은 별도 InputAction으로 분리한다.

  • 캐릭터: InputAction 바인딩 관리
  • 컨트롤러: IMC(Input Mapping Context) 및 매핑 관리

몽타주 연결 방식


UPROPERTY로 선언하고 BP 디테일 패널에서 직접 할당한다.

1
2
3
// CombatCharacter.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
UAnimMontage* ComboAttackMontage;

할당된 몽타주 애니메이션을 호출만 하는 식.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// CombatCharacter.cpp
void ACombatCharacter::ComboAttack()
{
    bIsAttacking = true;
    ComboCount = 0;

    NotifyEnemiesOfIncomingAttack();

    if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
    {
        const float MontageLength = AnimInstance->Montage_Play(
            ComboAttackMontage, 1.0f,
            EMontagePlayReturnType::MontageLength, 0.0f, true);

        if (MontageLength > 0.0f)
        {
            AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ComboAttackMontage);
        }
    }
}

AnimNotify로 함수 호출하기


몽타주에서 함수를 직접 호출할 수 없다.
UAnimNotify를 상속한 C++ 클래스를 만들고 Notify()를 오버라이드하면
컴파일 후 몽타주 노티파이 목록에 자동으로 등장한다.

1
2
3
4
5
6
7
8
// ANS_DoAttackTrace.h
UCLASS()
class UANS_DoAttackTrace : public UAnimNotify
{
    GENERATED_BODY()
public:
    virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override;
};
1
2
3
4
5
6
7
8
9
// ANS_DoAttackTrace.cpp
void UANS_DoAttackTrace::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
    AActor* Owner = MeshComp->GetOwner();
    if (ICombatAttacker* Attacker = Cast<ICombatAttacker>(Owner))
    {
        Attacker->DoAttackTrace();
    }
}

노티파이가 ICombatAttacker만 알면 되기 때문에,
캐릭터든 적이든 인터페이스를 구현한 액터라면 이 노티파이 하나로 공유할 수 있다.


인터페이스 설계


ICombatAttacker

공격 판정 로직을 인터페이스로 분리한다.
AnimNotify가 이 인터페이스만 알면 어떤 액터든 공격 판정을 위임할 수 있다.

1
2
3
4
5
6
7
8
9
UINTERFACE()
class UCombatAttacker : public UInterface { GENERATED_BODY() };

class ICombatAttacker
{
    GENERATED_BODY()
public:
    virtual void DoAttackTrace() = 0;
};

ICombatDamageable

피격 가능한 모든 액터가 구현하는 인터페이스.
공격자는 대상이 캐릭터인지 오브젝트인지 몰라도 데미지를 전달할 수 있다.

1
2
3
4
5
6
7
8
9
UINTERFACE()
class UCombatDamageable : public UInterface { GENERATED_BODY() };

class ICombatDamageable
{
    GENERATED_BODY()
public:
    virtual void ApplyDamage(float DamageAmount, AActor* DamageCauser) = 0;
};

공격 판정 구현


전방 적 사전 탐지

공격 시작 시점에 전방의 적을 미리 탐지해 대응할 수 있게 알린다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void ACombatCharacter::NotifyEnemiesOfIncomingAttack()
{
    TArray<FHitResult> OutHits;

    const FVector TraceStart = GetActorLocation();
    const FVector TraceEnd = TraceStart + (GetActorForwardVector() * DangerTraceDistance);

    FCollisionObjectQueryParams ObjectParams;
    ObjectParams.AddObjectTypesToQuery(ECC_Pawn);

    FCollisionShape CollisionShape;
    CollisionShape.SetSphere(DangerTraceRadius);

    FCollisionQueryParams QueryParams;
    QueryParams.AddIgnoredActor(this);

    if (GetWorld()->SweepMultiByObjectType(OutHits, TraceStart, TraceEnd,
        FQuat::Identity, ObjectParams, CollisionShape, QueryParams))
    {
        for (const FHitResult& CurrentHit : OutHits)
        {
            ICombatDamageable* Damageable = Cast<ICombatDamageable>(CurrentHit.GetActor());
            if (Damageable)
            {
                // 피격자 들에게 Notify 제공
                Damageable->NotifyDanger(GetActorLocation(), this);
                
                // 이러면 Shoot RayTrace 할 때도, 미리 Notify를 한 뒤에 데미지 Apply를 하는 식으로 구현하면
                // AI에게 회피기능을 제공할 수도 있을 것 같다
            }
        }
    }
}

DoAttackTrace

AnimNotify 발동 시 실제 히트 판정을 수행한다.
구체 스윕으로 범위 내 적을 감지하고 ApplyDamage()로 데미지를 전달한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void ACombatCharacter::DoAttackTrace()
{
    TArray<FHitResult> OutHits;

    const FVector TraceStart = GetActorLocation();
    const FVector TraceEnd = TraceStart + (GetActorForwardVector() * AttackTraceDistance);

    FCollisionShape CollisionShape;
    CollisionShape.SetSphere(AttackTraceRadius);

    FCollisionQueryParams QueryParams;
    QueryParams.AddIgnoredActor(this);

    if (GetWorld()->SweepMultiByChannel(OutHits, TraceStart, TraceEnd,
        FQuat::Identity, ECC_Pawn, CollisionShape, QueryParams))
    {
        for (const FHitResult& Hit : OutHits)
        {
            if (ICombatDamageable* Damageable = Cast<ICombatDamageable>(Hit.GetActor()))
            {
                Damageable->ApplyDamage(AttackDamage, this);
            }
        }
    }
}
This post is licensed under CC BY 4.0 by the author.