레퍼런스

델리게이트 노티파이

UNREAL 2023. 12. 23. 03:08

https://velog.io/@cedongne/UE5-Unreal-Engine-5-%EA%B8%B8%EB%9D%BC%EC%9E%A1%EC%9D%B4-12.-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%AA%BD%ED%83%80%EC%A3%BC-%EB%8D%B8%EB%A6%AC%EA%B2%8C%EC%9D%B4%ED%8A%B8-%EB%85%B8%ED%8B%B0%ED%8C%8C%EC%9D%B4

 

[UE5] Unreal Engine 5 길라잡이 - 12. 애니메이션 몽타주, 델리게이트, 노티파이

애니메이션 몽타주와 함께 언리얼 엔진의 델리게이트, 노티파이 등이 무엇인지 알아보자.

velog.io

이 시리즈는 이득우의 언리얼 C++ 게임 개발의 정석을 바탕으로 작성되었습니다.

· 애니메이션 몽타주

애니메이션 몽타주(Animation Montage)는 정해진 순서에 따라 실행되는 애니메이션 사이에서 조건에 맞게 애니메이션 재생을 조절하는 기능이다. 연속으로 재생해야 하는 여러 애니메이션 중 일부를 반복하거나 길게 재생하거나, 특정 애니메이션의 생략 혹은 조기 종료 등의 처리를 쉽게 해준다.

콘텐츠 브라우저에서 우클릭해 해당 애셋을 생성할 수 있다.

애니메이션 몽타주 윈도우를 열고 순서대로 재생할 애니메이션을 적절하게 배치해준다. 애셋 브라우저 패널에서 애니메이션을 드래그해 몽타주 타임라인의 DefaultGroup.DefaultSlot에 놓는다.

이번에 구현할 애니메이션은 콤보 공격 애니메이션이다.

- 몽타주 섹션

타임라인에 애니메이션이 위치한 윗부분인 보라색 영역은 몽타주 섹션을 표현한다. 이는 애니메이션을 재생할 별도의 단위를 구축하는 것이고, 원하는 길이의 섹션을 생성할 수 있다.

이번 예제에서 각 애니메이션을 모두 다른 섹션으로 설정할 것이므로, 기존의 Default 섹션을 선택해 이름을 Attack1으로 변경하고 나머지 애니메이션을 각각의 섹션으로 분리해준다.

섹션을 생성한다고 완전히 분리되는 것은 아니다. 연속된 섹션은 기본적으로 특정 섹션의 재생이 끝나고 다음 섹션으로 자동으로 이동하기 때문에, 입력이 들어오지 않으면 콤보 공격 중에도 재생을 중단해야 하는 이번 애니메이션에선 각 섹션을 완전히 분리할 필요가 있다.

이때 타임라인에서 섹션 부분을 잘 보면, 각 섹션의 말단에 화살표 아이콘을 확인할 수 있다. 이는 해당 섹션이 끝나면 다음 섹션으로 자동으로 이동함을 의미한다.

타임라인의 우측 몽타주 섹션 패널에서 다음 섹션 설정을 해제해 줄 수 있다.

특정 섹션 이후에 존재하는 화살표 아이콘을 클릭하면 다음 섹션을 선택할 수 있는 팝업을 확인할 수 있다. 이때 링크 제거를 선택하면 해당 섹션 실행 후 다음 섹션으로 이동하지 않고 몽타주 재생을 바로 종료할 수 있다.

모든 섹션의 링크를 제거해 명령이 들어오지 않으면 몽타주가 계속 재생되지 않도록 설정해준다.

이렇게 처리해주면 프리뷰에서 첫 번째 섹션만 반복 실행되는 것을 확인할 수 있다. 여기서 다음 섹션을 실행하고 싶다면 섹션 이름을 변수로 관리하면서 애님 인스턴스 클래스에서 특정 섹션으로 이동하는 함수를 정의해주면 된다.

특정 섹션으로 이동하는 실제 함수는 UAnimInstance::Montage_JumpToSection(섹션명, 대상 몽타주)로 선언되어 있다.

· 델리게이트

델리게이트(Delegate)는 널리 사용되는 프로그래밍 용어로, 특정 객체가 해야 할 로직을 다른 객체가 대신 처리할 수 있도록 만드는 설계를 말한다. C#과 같은 언어는 델리게이트 시스템을 기본적으로 제공하지만, C++은 이를 지원하지 않아 언리얼 엔진에서 별도로 구축한 델리게이트 프레임워크를 사용한다.

언리얼 엔진의 델리게이트는 A 객체가 B 객체에게 특정 명령을 내릴 때 직접 명령을 내리지 않고도 B 객체에 자신을 등록하고 B 객체가 조건에 따라 해당 명령을 수행하면 A 객체에게 그 사실을 알아서 전달한다. 즉, 원래 A가 B의 멤버를 호출하고 그 반환값을 받는 것이 일반적이지만, 델리게이트는 멤버를 호출한다는 작업이 생략되고도 반환을 받을 수 있다.

애님 인스턴스는 애니메이션 몽타주 재생이 끝나면 발동하는 OnMontageEnded라는 델리게이트를 제공한다. 어떤 언리얼 오브젝트라도 (UAnimMontage *, bool) 매개변수 조합의 멤버 함수를 가지고 있다면 이를 OnMontageEnded 델리게이트에 등록해 몽타주 재생이 끝나는 타이밍을 파악할 수 있다. 위 예시에서 각 섹션이 연결돼있지 않기 때문에 전체 몽타주가 아니라 특정 섹션이 종료되면 OnMontageEnded 델리게이트가 호출된다.

몽타주 재생이 끝나는 타이밍을 알아낼 액터 클래스에 UFUNCTION 매크로를 추가한 델리게이트 함수를 선언한다.

- 다이나믹 델리게이트

// MyCharacter.h

UFUNCTION()
void OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);

UFUNCTION 매크로를 추가하는 것은 블루프린트 객체도 델리게이트를 사용할 수 있게 하기 위함이다. 언리얼 델리게이트는 C++ 객체에만 사용할 수 있는 델리게이트와 블루프린트 객체도 함께 사용할 수 있는 델리게이트로 나뉘는데, OnMontageEnded 델리게이트는 블루프린트의 함수화도 연동할 수 있도록 설계된 다이나믹 델리게이트(Dynamic Delegate)이다.

블루프린트 객체는 메서드에 대한 정보를 저장하고 로딩할 때 직렬화(Serialization)라는 메커니즘이 동반되어 별도의 메서드 관리 방법이 필요하다. 이를 위해 블루프린트에 관련된 메서드는 UFUNCTION 매크로를 사용해 관리하는데, 이것은 델리게이트에도 똑같이 적용된다.

- 델리게이트 바인딩

델리게이트로 사용하기 위한 함수를 선언하였으면 앞서 말한 것처럼 함수 자신을 델리게이트에 등록할 필요가 있다. 이것을 바인딩이라 하며, 다음과 같이 PostInitializeComponents() 혹은 BeginPlay() 등의 초기화 함수에 바인딩을 해준다.

// MyCharacter.h

UCLASS()
class UE5PRACTICE_API AMyCharacter : public ACharacter
{
	GENERATED_BODY()
	
    ...

private:
	
    ...
	
    void OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);

private:
	...

	UPROPERTY()
	class UMyAnimInstance* MyAnim;
};
// MyCharacter.cpp

void AMyCharacter::PostInitializeComponents() {
	Super::PostInitializeComponents();
    
	MyAnim = Cast<UMyAnimInstance>(GetMesh()->GetAnimInstance());
	check(nullptr != MyAnim);
	MyAnim->OnMontageEnded.AddDynamic(this, &AMyCharacter::OnAttackMontageEnded);
}

이때 스크립트 편집기로 VS를 사용하고 있다면 OnMontageEnded.AddDynamic() 매크로를 찾지 못하고 OnMontageEnded.__Internal_AddDynamic()만 VS 인텔리센스 식별자 목록에서 발견할 수 있을 것인데, VS 인텔리센스에서 이를 지원하지 않을 뿐이니 무시하고 AddDynamic()으로 작성해도 문제 없다.

OnMontageEnded 델리게이트는 애님 인스턴스가 제공하므로 먼저 캐릭터의 애님 인스턴스를 찾고 그 안에서 델리게이트를 찾아 다이나믹으로 델리게이트용 함수를 등록해주면 된다. AddDynamic() 매크로의 인자는 (대상 오브젝트, 등록할 함수) 쌍으로 구성된다.

MyAnimInstance 클래스를 멤버 변수로 선언할 때 헤더에 MyAnimInstance.h를 포함하는 것이 아닌 선언 자체에 class 키워드를 포함하는 전방 선언으로 설계하였는데, 이는 해당 헤더 파일에서 이미 가지고 있는 EngineMinimal.h(현재 예시에선 UE5Practice.h) 와 같은 헤더 파일을 두 번 참조하지 않아도 되므로 상호 참조를 방지하고 코드 구조를 관리하기도 편하다.

- 멀티캐스트 델리게이트

OnMontageEnded 델리게이트는 블루프린트 호환이 가능한 다이나믹 델리게이트라는 성질 외에도 여러 개의 함수를 받을 수 있어서 행동이 끝나면 등록된 모든 함수들에 신호를 주는 기능도 제공한다. 이러한 델리게이트를 멀티캐스트 델리게이트(Multicast Delegate)라고 한다.

- 델리게이트 시그니처

언리얼 엔진에는 수많은 델리게이트가 구현되어 있기 때문에 각 델리게이트가 어떤 성질을 가지고 있는지 헷갈릴 텐데, 델리게이트에 마우스를 올려 보면 델리게이트의 실제 클래스명을 확인할 수 있다.

그리고 델리게이트 식별자에 Ctrl + 마우스 좌클릭 단축키를 사용해 정의로 이동할 수 있는데,

해당 타입의 언리얼 오브젝트에서 어떤 델리게이트가 어떤 성질을 가질 것인지 위와 같이 정의하고 실제로 사용할 식별자로 선언하여 간단한 형태로 사용한다. 이렇게 필요한 용도로 오브젝트에 정의된 델리게이트의 형식을 시그니처(Signature)라 한다.

OnMontageEnded 시그니처는 다이나믹 멀티캐스트 델리게이트라고 할 수 있다.

· 애니메이션 노티파이

애니메이션 노티파이(Animation Notify), 줄여서 노티파이는 언리얼 엔진에서 애니메이션을 재생하는 동안 특정 타이밍에 애님 인스턴스에게 신호를 보내는 기능이다. 걷기나 달리기 중에 발소리 같은 이펙트를 추가하거나, 애니메이션 도중 파티클 시스템을 스폰하는 등 일반 애니메이션에도 노티파이를 사용할 수 있지만, 몽타주를 다루고 있는 만큼 함께 응용하는 방법을 배워보자.

이번 예제에서 콤보 공격을 구현하고 있으므로 공격 애니메이션 재생 중 특정 타이밍에 공격 판정을 할 수 있도록 노티파이를 활용해보자.

앞서 다뤘던 WarriorAttackMontage 애셋을 열어 애니메이션 몽타주 윈도우를 열고, 하단 타임라인에서 노티파이 트랙 섹션에 오른쪽 클릭을 한다.

애니메이션 몽타주엔 기본적으로 1이라는 이름의 노티파이 트랙이 존재하고(이름은 변경할 수 있다), 해당 트랙의 타임라인 부분을 우클릭하면 된다. 대충 빨간 상자가 있는 부분을 클릭한다고 보면 된다.

그럼 위와 같이 새로운 노티파이가 추가되었고, 이름은 AttackHitCheck로 설정해주었다. 이제 시스템에서 해당 몽타주 애니메이션을 재생하면 타임라인에 위치한 노티파이를 호출하게 되고, 노티파이가 호출되면 엔진에서 자동으로 애님 인스턴스 클래스의 AnimNotify_노티파이명이라는 이름의 멤버 함수를 찾아 호출한다.

사용자가 노티파이 실행 타이밍을 알아내 함수를 호출하는 것이 아니라, 엔진이 함수를 호출하도록 만드는 것이기 때문에 노티파이도 일종의 델리게이트를 활용하는 것이라고 볼 수 있다.

사용자가 델리게이트에 등록할 함수를 원하는 언리얼 오브젝트에 직접 선언하고 등록해줬던 것처럼 노티파이에 의해 호출될 멤버 함수를 원하는 애님 인스턴스 클래스에 선언해주면 된다. 또한 델리게이트와 같은 이유로 UFUNCTION() 매크로를 지정하는 것도 잊지 말자.

// MyAnimInsatnce.h

UCLASS()
class UE5PRACTICE_API UMyAnimInstance : public UAnimInstance
{
	...
    
private:
	UFUNCTION()
	void AnimNotify_AttackHitCheck();
    
    ...
};

헷갈리지 말아야 할 것은 애님 인스턴스의 OnMontageEnded 델리게이트에 등록할 멤버 함수는 MyCharacter 클래스에 선언했지만 이번 노티파이 함수는 MyAnimInstance 클래스 자체에 선언했다.

노티파이 함수는 미리 정의되지 않았고 델리게이트는 미리 정의되었기 때문에 이러한 차이가 발생한 것이며, 다른 경우처럼 보이지만 두 경우 모두 사용자가 직접 호출하는 것이 아니라 언리얼 엔진에서 호출하기 때문에 애님 인스턴스 클래스 내부에 위치하는 것이다.

- 커스텀 델리게이트

엔진이 호출할 노티파이 함수를 애님 인스턴스 클래스에 선언한 것처럼 델리게이트도 사용자가 직접 정의하여 커스텀 애님 인스턴스 클래스에 포함해줄 수 있다. 앞서 살펴본 OnMontageEnded 델리게이트가 어떻게 선언되어 있었는지 참고한다면 그 방법은 간단하다.

// MyAnimInstance.h
#pragma once

#include "UE5Practice.h"
#include "Animation/AnimInstance.h"
#include "MyAnimInstance.generated.h"

DECLARE_MULTICAST_DELEGATE(FOnNextAttackCheckDelegate);
DECLARE_MULTICAST_DELEGATE(FOnAttackHitCheckDelegate);

UCLASS()
class UE5PRACTICE_API UMyAnimInstance : public UAnimInstance
{
	...
    
public:
	FOnNextAttackCheckDelegate OnNextAttackCheck;
	FOnAttackHitCheckDelegate OnAttackHitCheck;
    
    ...
}

위와 같이 여러 개의 함수를 동시에 등록할 수 있는 델리게이트를 선언하였고, 언리얼 엔진에서 마련해 둔 델리게이트는 호출 타이밍이 정해져 있지만 커스텀 델리게이트는 호출 타이밍도 지정해주어야 한다.

멀티캐스트 델리게이트에 등록된 모든 함수를 호출하는 명령은 BroadCast 멤버 함수를 호출하는 것이다.

// MyAnimInstance.cpp

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

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

예를 들어 이와 같이 설계하면 AttackHitCheck / NextAttackCheck 노티파이가 발생할 때마다 선언해 둔 멀티캐스트 델리게이트에 등록된 모든 함수를 호출한다.

- 몽타주 틱 타입

이번 예시는 입력에 맞게 특정 순서의 애니메이션을 정확히 실행해야 한다. 따라서 노티파이도 한 프레임의 오차 없이 발생해야 한다.

이것을 위해 노티파이의 몽타주 틱 타입을 Queued(대기열 방식)에서 Branching Point(분기점 방식)으로 변경해 줄 필요가 있다.

해당 노티파이에 도달하는 정확한 시점, 그리고 이동할 애니메이션의 정확한 지점으로 이동해야 하는 섹션을 바꾸거나 몽타주 위치를 변경하는 작업에는 분기점 방식이 더 적합하다.

대기열 방식은 비동기적인 속성 때문에 정밀도가 떨어지지만, 분기점 방식보다 퍼포먼스 비용이 낮다. 이러한 트레이드오프를 잘 고려하여 적절한 몽타주 틱 타입을 선택하는 것이 옳다.


· 참고

 [UE Document] 애니메이션 몽타주 개요
 [C++] 전방선언 (Forward Declaration)
 [UE Document] 애니메이션 노티파이