본문 바로가기

언리얼C++게임개발/08.애니메이션 시스템 활용

애니메이션 노티파이

애니메이션 노티파이

애니메이션 노티파이는 애니메이션을 재생하는 동안 특정 타이밍에 애님 인스턴스에게 신호를 보낼 수 있는 기능이다. 애니메이션 노티파이는 일반 애니메이션과 몽타주 모두 사용 가능한데, 여기서는 노티파이를 이용해 콤포 공격을 구현해 보자.

 

먼저, 위와 같이 NextAttackCheck 노티파이를 만든다. 공격 중, NextAttackCheck 노티파이가 있는 영역에서 공격 버튼이 눌릴 경우, 다음 섹션으로 넘어가 콤보 공격을 가능하도록 만드는 것이 목표이다. AttackHitCheck 노티파이는 그냥 노티파이가 작동했는지 로그를 찍어주는 기능으로 구현할 것이다.

 

컴파일후 플레이를 통해 공격을 해보면 몽타주 재생에 따라 네번의 노티파이가 발생함을 확인할 수 있다.

노티파이가 호출되면 언리얼은 자동으로 애님 인스턴스 클래스의 'AnimNotify_노티파이명' 이라는 이름의 멤버 함수를 찾아서 호출한다.

콤보 공격의 구현

각 공격 동작을 섹션으로 분리후 콤보공격을 구현해보자. Attack2, Attack3, Attack4 섹션을 추가하고 각 섹션마다 애니메이션을 하나씩 할당하였다. 그리고 섹션창에서 하나로 연결되어있던 섹션들을 분리하였고, NextAttackCheck 노티파이를 추가하였다.

이때 멤버 함수는 언리얼 런타임이 찾을 수 이도록 UFUNCTION 매크로가 지정돼야 한다. 다이나믹 델리게이트에서처럼, C++ 와 블루프린트 객체 모두에서 사용가능할 수 있어야 하기 때문이다.

 

애니메이션 노티파이를 설정한 후에는 해당 프레임에 즉각적으로 반응하는 방식인 Branching Point 값으로 틱 타입(Tick Type)을 변경하는 것이 좋다. 기본값인 Queued 로 설정하게 되면 비동기 방식으로 신호를 받게 돼서 적절한 타이밍에 신호를 받는 것을 놓치게 될 수 있다. Queued 값은 주로 타이밍에 민감하지 않은 사운드나 이펙트를 발생시킬 때 사용하는 것이 적합하다.

그리고 예제를 따라 코드를 작성하였다. 작성한 코드는 Github에 올려두었다. 플레이해보니 애니메이션 속도가 빨라서인지 콤보공격을할 타이밍을 잡기가 어려워 애니메이션 속도를 0.6배속으로 재생하도록 하였다.

 

구현시 어려웠던 부분

처음 했는데 계속 다음 콤보로 안넘어가는거다...
NextAttackCheck가 섹션의 중간쯤에는 있어야 한다.
너무 뒤에 있으면 다음 섹션을 호출하기 전에 OnMontageEnded가 호출되어서
콤보 공격이 실행이 안된다.
그래서 애먹었다..
처음에 계속 첫번째것만 하고 로그는 찍히는데
Montage_IsPlay에서 오류가 뜨는고임
근데 알고보니 너무 뒤에 놔서 그런거였음..

ABCharacter.h

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{

...

private:
    
    ...
    
    void AttackStartComboState();
    void AttackEndComboState();
   
private:

    UPROPERTY(VisibleInstanceOnly, BluePrintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
    bool CanNextCombo;
    
    UPROPERTY(VisibleInstanceOnly, BluePrintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
    bool IsComboInputOn;
    
    UPROPERTY(VisibleInstanceOnly, BluePrintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
    int32 CurrentCombo;
    
    UPROPERTY(VisibleInstanceOnly, BluePrintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
    int32 MaxCombo;
}

먼저, Character.h 에 AttackComboState, AttackEndState 멤버 함수와 CanNextCombo, IsComboInputOn, CurrentCombo, MaxCombo 변수를 선언한다.

 

ABCharacter.cpp

...

AABCharacter::AABCharacter()
{
    ...
    MaxCombo = 4;
    AttackEndComboState();
}

...

void AABCharacter::AttackStartComboState()
{
    CanNextCombo = true;
    IsComboInputOn = false;
    ABCHECK(FMath::IsWithinInclusive<int32>(CurrentCombo, 0, MaxCombo - 1));
    CurrentCombo = FMath::Clamp<int32>(CurrentCombo + 1, 1, MaxCombo);
}

void AABCharacter::AttackEndComboState()
{
    IsComboInputOn = false;
    CanNextCombo = false;
    CurrentCombo = 0;
}

공격이 시작하거나 끝날 때 사용할 함수와 변수를 모두 선언했다. 이때 FMath::Clamp 라는 표현식은 값을 받아 최소치와 최대치로 정의된 특정 범위로 제한시킨다. 최소가 1, 최대가 4이면, 결과값은 절대 1 미만이거나 4 초과가 되지 않는다.

 

이제 ABAnimInstance 클래스에서 콤보 카운트를 전달받으면 해당 몽타주 섹션을 재생하도록 기능을 구현한다. 앞서 선언한 NextAttackCheck 노티파이가 발생할 때마다 ABCharacter 에 이를 전달할 델리게이트를 선언하고 애니메이션 노티파이 함수에서 이를 호출한다.

 

이렇게 델리게이트 기능을 사용하면, 애님 인스턴스는 자신의 델리게이트를 사용하는 객체가 어떤 것인지 몰라도 델리게이트에 연결된 함수만 호출하면 되므로, 다른 클래스와 연결되지 않는 의존성 없는 설계를 할 수 있다는 장점이 생긴다.

반환 값과 인자 값이 없는 함수 유형으로 델리게이트를 선언하되, 여러 개의 함수가 등록되도록 멀티캐스트로 선언한다. 멀티캐스트 델리게이트에 등록된 모든 함수를 호출하는 멀티캐스트 델리게이트 명령은 Broadcast 이다.

 

ABAnimInstance.h

...

DECLARE_MULTICAST_DELEGATE(FOnNextAttackCheckDelegate);
DECLARE_MULTICAST_DELEGATE(FOnAttackHitCheckDelegate);

...

UCLASS()
class ARENABATTLE_API UABAnimInstance : public UAnimInstance
{

    ...
    
public:
    void JumpToAttackMontageSection(int32 NewSection);
    
public:
    FOnNextAttackCheckDelegate OnNextAttackCheck;
    FOnAttackHitCheckDelegate OnAttackHitCheck;
    
    ...
    
private:
    
    UFUCNTION()
    void AnimNotify_AttackHitCheck();
    
    UFUNCTION()
    void AnimNotify_NextAttackCheck();
    
    FName GetAttackMontageSectionName(int32 Section);

}

FOnNextAttackCheckDelegate 와 FOnAttackHitCheckDelegate 를 선언했다.

 

ABAnimInstance.cpp

...

void UABAnimInstance::JumpToAttackMontageSection(int32 NewSection)
{
    ABCHECK(Montage_IsPlaying(AttackMontage));
    Montage_JumpToSection(GetAttackMontageSectionName(NewSection), AttackMontage);
}

void UABAnimInstance::AnimNotify_AttackHitCheck()
{
    OnAttackHitCheck.Broadcast();
}

void UABAnimInstance::AnimNotify_NextAttackCheck()
{
    OnNextAttackCheck.Broadcast();
}

FName UABAnimInstance::GetAttackMontageSectionName(int32 Section)
{
    ABCHECK(FMath::IsWithinInclusive<int32>(Section, 1, 4), NAME_None);
    return FName(*FString::Printf(TEXT("Attack%d"), Section));
}

 

이제 플레이어가 공격 명령을 내리면 ABCharacter 는 콤보가 가능한지 아닌지 파악하고 각 상황에 대응한다. 

 

ABCharacter.cpp

...

void AABCharacter::PostInitializeComponents()
{

    ...
    
    ABAnim->OnNextAttackCheck.AddLambda([this]() -> void {
        ABLOG(Warning, TEXT("OnNextAttackCheck"));
        CanNextCombo = false;
        
        if (IsComboInputOn)
        {
            AttackStartComboState();
            ABAnim->JumpToAttackMontageSection(CurrentCombo);
        }
        
    });
}

...

void AABCharacter::Attack()
{
    if (IsAttacking)
    {
    	ABCHECK(FMath::IsWithinInclusive<int32>(CurrentCombo, 1, MaxCombo));
        if (CanNextCombo)
        {
            IsComboInputOn = true;
        }
    }
    else
    {
        ABCHECK(CurrentCombo == 0);
        AttackStartComboState();
        ABAnim->PlayAttackMontage();
        ABAnim->JumpToAttackMontageSection(CurrentCombo);
        IsAttacking = true;
    }
}

void AABCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
    ABCHECK(IsAttacking);
    ABCHECK(CurrentCombo > 0);
    IsAttacking = false;
    AttackEndComboState();
}

ABAnimInstance 의 OnNextAttackCheck 델리게이트와 등록할 로직을 ABCharacter 에서 선언하고 구현했다. 이때 람다식을 이용했는데, 참조할 환경 (캡쳐, Capture) 는 this 로 지정한다. 람다 구문에서 인스턴스의 관련 멤버 함수와 변수를 사용하기 때문이다.