3 minute read

‘이득우의 언리얼 c++ 게임 개발의 정석’ 책을 참고하여 작성한 포스트입니다.


애니메이션 시스템에는 애니메이션 몽타주, 노티파이와 같은 프로그래밍과연동해 복잡한 설계를 간편하게 만들어주는 기능이 있다.
콤보 공격 등에 활용하기 좋다.


애니메이션 몽타주

  • 애니메이션 몽타주는 스테이트 머신의 확장 없이 특정 상황에서 원하는 애니메이션을 발동시킬 수 있게 해준다.
  • 먼저, 몽타주 에셋이 있어야 한다. 애니메이션 창에서 ‘애님 몽타주’ 생성 메뉴를 눌러 몽타주 에셋을 생성해준다.
  • 몽타주는 섹션(section)을 단위로 애니메이션을 관리한다. 애니메이션 클립들의 일부를 떼어내고 붙여 새로운 애니메이션을 생성하는 기법이다.
  • 섹션 하단에 원하는 애니메이션들을 드래그해 배치해준다.
  • 각 애니메이션을 클릭하면 애니메이션 세그먼트를 볼 수 있는데 여기서 애니메이션 재생을 조절할 수 있다.
  • 섹션 이름을 이용해 필요할 때 재생 가능하다.
  • 애님 인스턴스에 멤버 함수와 변수를 생성하고 함수 실행시 몽타주 애니메이션을 재생하도록 해본다.
  • 몽타주 애셋과 관련된 명령은 항상 몽타주 애셋을 참조하므로 생성자에서 미리 설정해두면 편리하다.
  • 애님 그래프에 몽타주 재생 노드를 추가해야 애니메이션 블루프린트에서 몽타주를 재생한다.
  • 몽타주의 ‘defaultslot’ 노드를 최종 애니메이션 포즈와 스테이트 머신 사이에 생성해주고 최종 애니메이션 포즈에 결과를 연결해주면 된다.
// ABAnimInstance.h
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess=true))
UAnimMontage* AttackMontage;


// ABAnimInstance.cpp
static ConstructorHelpers::FObjectFinder<UAnimMontage> ATTACK_MONTAGE(TEXT("/Script/Engine.AnimMontage'/Game/Animations/SK_Mannequin_Skeleton_Montage.SK_Mannequin_Skeleton_Montage'"));
if(ATTACK_MONTAGE.Succeeded())
{
    AttackMontage = ATTACK_MONTAGE.Object;
}

void UABAnimInstance::PlayAttackMontage()
{
    /*
    if (!Montage_IsPlaying(AttackMontage))
    {
        Montage_Play(AttackMontage, 1.0f);
    }
    */

   ABCHECK(!IsDead);

   // 델리게이트로 공격의 시작과 종료를 감지하므로 위의 코드 대신
   Montage_Play(AttackMontage, 1.0f);
}

// ABCharacter.cpp
void AABCharacter::Attack()
{
	auto AnimInstance = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());

	if(nullptr == AnimInstance) return;

	AnimInstance->PlayAttackMontage();
}

UAnimInstance::Montage_IsPlaying

  • Returns true if the animation montage is currently active and playing.
bool Montage_IsPlaying
(
    const UAnimMontage * Montage
) const

UAnimInstance::Montage_Play

  • Plays an animation montage.
float Montage_Play
(
    UAnimMontage * MontageToPlay,
    float InPlayRate,
    EMontagePlayReturnType ReturnValueType,
    float InTimeToStartMontageAt,
    bool bStopAllMontages
)

델리게이트(delegate)

  • 넓은 의미의 델리게이트는 특정 객체가 해야 할 로직을 다른 객체가 대신 처리할 수 있도록 만드는 보편적인 설계의 개념이다.
  • 언리얼 엔진의 델리게이트는 A 객체가 B 객체에 작업 명령을 내릴 때 B 객체에 자신을 등록하고,
  • B의 작업이 끝나면 이때 A에게 알려주는 설계 방식을 의미한다.
  • 애님 인스턴스는 애니메이션 몽타주 재생이 끝나면 발동하는 OnMontageEnded 라는 델리게이트를 제공한다.
  • 언리얼 오브젝트에 UAnimMontage* 인자와 bool 인자를 가진 멤버 함수가 있다면, 이를 OnMontageEnded 델리게이트에 등록 가능하다.
  • 언리얼에서 델리게이트는 c++ 객체에만 사용가능한 델리게이트와 블루프린트 객체와 c++ 모두 사용 가능한 델리게이트가 있다.
  • 블루프린트 오브젝트는 멤버 함수에 대한 정보를 저장하고 로딩하는 직렬화 메커니즘이 들어있기 때문에 일반 c++ 언어가 관리하는 방법으로 멤버 함수를 관리할 수 없다.
  • 그래서 블루프린트와 관련된 c++ 함수는 UFUNCTION 매크로를 사용해야 한다. 이 델리게이트를 dynamic delegate라고 한다.
  • ABCharacter 클래스에 PostinitializeComponents에서 델리게이트에 바인딩 해본다.
  • OnMontageEnded는 multicast delegate이기 때문에 여러 개의 함수를 받아, 모든 함수들에게 알려줄 수 있다.
  • 언리얼엔진에서 델리게이트 선언은 매크로를 통해 정의되며, 이렇게 정의되는 델리게이트 형식을 Signiture 라고 한다.
  • ABCharacter 에서 ABAnimInstance 클래스를 자주 사용할 것이므로 이를 멤버 변수로 선언해 런타임에서 활용하도록 하면 좋다. 이렇게 멤버 변수로 선언할 때는 전방 선언으로 해주면 좋다. 전방 선언을 하면 헤더파일에서 같은 모듈에 있는 다른 헤더 파일을 참조하지 않아도 되므로 상호 참조를 방지하는 한편, 코드 구조 관리에도 용이하다.
// ABCharacter.h
private:
	UFUNCTION()
	void OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);

	// 전방 선언
	UPROPERTY()
	class UABAnimInstance* ABAnim;
	// 이후 ABAnimInstance 를 ABAnim으로 변경해준다.

// ABCharacter.cpp
void AABCharacter::PostInitializeComponents()
{
	Super::PostInitializeComponents();
	
	auto AnimInstance = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());
	ABCHECK(nullptr != AnimInstance);

	AnimInstance->OnMontageEnded.AddDynamic(this, &AABCharacter::OnAttackMontageEnded);
}


void AABCharacter::OnAttackMontageEnded(UAnimMontage * Montage, bool bInterrupted)
{
	ABCHECK(IsAttacking);
	// 공격이 끝났음을 알려준다.
	// true인 동안 몽타주 재생을 막는것을 구현해 둔 상태임
	IsAttacking = false;
}

USkeletalMeshComponent::GetAnimInstance

  • Returns the animation instance that is driving the class (if available).
UAnimInstance* GetAnimInstance() const

애니메이션 노티파이

  • 애니메이션을 재생하는 동안 특정 타이밍에 애님 인스턴스에 신호를 보내는 기능을 제공한다.
  • 일반 애니메이션, 몽타주 모두 사용 가능하다.
  • 애니메이션 몽타주 창에서 맨 아래에 노티파이 창이 있다.
  • 원하는 타이밍에 노티파이를 설정 해두면 해당 몽타주 애니메이션 재생시 재생 구간에 위치한 노티파이를 호출한다.
  • 노티파이가 호출되면 애님 인스턴스 클래스의 ‘AnimNotify_노티파이명’ 이름의 멤버 함수를 찾아 호출한다.
  • 이 멤버 함수는 언리얼 런타임이 찾을 수 있도록 UFUNCTION 매크로가 지정되어야 한다.

콤보 공격의 구현

  • 섹션을 추가해 각 공격 동작을 분리하고, 각 섹션마다 애니메이션을 하나씩 할당한다. 그래야 콤보가 끊기면 idle 애니메이션으로 돌아간다.
  • 섹션 사이에 있는 녹색 x 버튼을 누르면 섹션들이 연속해서 재생되지 않게 된다.
  • 애니메이션 노티파이 설정 후에는 해당 프레임에 즉각적으로 반응하는 방식인 Branching Point 값으로 Tick Type을 변경해 주는 것이 좋다. 기본 값인 Queued는 비동기 방식으로 신호를 받아 적절한 타이밍에 신호를 받는 것을 놓칠 수 있다. Queue 값은 타이밍에 민감하지 않은 사운드나 이펙트 발생할 때 사용하는 것이 낫다.
  • 반환값과 인자 값이 없는 함수 유형으로 멀티캐스트 델리게이트를 선언하고, 람다 함수를 이용해 등록해 보자. ABAnimInstance에 델리게이트를 선언하고, ABCharacter에서 메서드를 등록한다고 하고,
  • 애님 인스턴스에서 노티파이 호출시마다 델리게이트를 이용해 등록된 모든 함수를 호출하게 한다. 그러면 애님 인스턴스는 자신의 델리게이트를 사용하는 객체를 알 필요 없이 연결된 함수 호출만 하면 되므로,
  • 다른 클래스와 연결되지 않는 의존성 없는 설계를 할 수 있는 장점이 생긴다.
// ABAnimInstance.h
DECLARE_MULTICAST_DELEGATE(FOnNextAttackCheckDelegate);
DECLARE_MULTICAST_DELEGATE(FOnAttackHitCheckDelegate);

public:
	FOnNextAttackCheckDelegate OnNextAttackCheck;
	FOnAttackHitCheckDelegate OnAttackHitCheck;

// ABAnimInstance.cpp
void UABAnimInstance::AnimNotify_AttackHitCheck()
{
    ABLOG_S(Warning);
    OnAttackHitCheck.Broadcast();
}

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

// ABCharacter.cpp
ABAnim->OnNextAttackCheck.AddLambda([this]() -> void {
		ABLOG(Warning, TEXT("OnNextAttackCheck"));
		CanNextCombo = false;

		if (IsComboInputOn)
		{
			AttackStartComboState();
			ABAnim->JumpToAttackMontageSection(CurrentCombo);
		}
	});

ABAnim->OnAttackHitCheck.AddUObject(this, &AABCharacter::AttackCheck);

	CharacterStat->OnHPIsZero.AddLambda([this]() -> void {
		ABLOG(Warning, TEXT("OnHpIsZero"));
		ABAnim->SetDeadAnim();
		SetActorEnableCollision(false);
	});

Leave a comment