개요
'이득우의 언리얼 C++ 게임 개발의 정석'의 8-9장의 내용을 바탕으로 하고 있습니다.
본문
연속된 애니메이션을 만들 때 스테이트 머신을 사용해서 모든 애니메이션에 대한 스테이트를 생성하고 트랜지션을 연계해서
구현할 수도 있지만, 설계가 복잡해진다.
언리얼 엔진에서는 이런 경우를 위해 스테이트 머신 확장 없이 특정한 상황에 원하는 애니메이션을 발동 시키는
애니메이션 몽타주라는 기능을 제공한다.
애니메이션 애셋에서 애셋 생성 → 애님 몽타주를 선택해서 몽타주를 생성한다.
몽타주는 섹션을 단위로 애니메이션을 관리한다.
하단의 몽타주(DefaultGroup) 옆의 Default라 되어있던 노드를 선택하면 디테일 창에 섹션에 대한 정보가 나온다.
섹션명을 Attack1로 변경하고 그 아래 슬롯에 애셋 브라우저에서 사용할 공격 애니메이션들을 차례대로 끌어다 넣어준다.
애니메이션이 자연스럽게 이어지도록 각 끌어다놓은 애니메이션들을 클릭하여 끝 시간을 조절한다.
저장한 결과는 Attack1이라는 섹션으로 저장되어 공격 명령을 누를 때 섹션 이름을 사용해 필요할 때 재생시킬 수 있다.
마우스 왼 클릭으로 공격 입력을 추가하기 위해 Input Action으로 IA_Player_Attack을 추가하고 이를 입력 매핑 컨텍스트에 추가해주었다.
키는 왼쪽 마우스 버튼을 바인딩하고 트리거로 눌림을 넣었다.
이를 바탕으로 ABCharacter 클래스에 AttackAction과 Attack 함수를 만들어줘서 액션을 바인딩해줬다.
그리고 애님 인스턴스 클래스에서 몽타주 애니메이션을 재생하도록 기능을 구현한다.
// ABAnimInstance.cpp
#include "ABAnimInstance.h"
UABAnimInstance::UABAnimInstance()
{
CurrentPawnSpeed = 0.0f;
IsInAir = false;
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()
{
// AnimInstance에 구현되어 있는 Montage가 현재 재생중인지 판별하는 메서드
if (!Montage_IsPlaying(AttackMontage))
{
// 매개변수로 들어온 AttackMontage 몽타주를 원본 재생 길이의 배율 1으로 재생한다.
Montage_Play(AttackMontage, 1.0f);
}
}
컴파일을 마치고 나면 애니메이션 블루프린트로 가보면 Attack 하위에 Attack Montage가 있고 기본값으로 Attack Montage에 만들어둔 Attack 몽타주 애셋이 할당되어 있음을 확인할 수 있다.
코드 상으로 몽타주를 재생시키더라도 애니메이션 블루프린트의 애님 그래프에 추가를 해야 실제로 재생이 된다.
애니메이션 노드 목록에서 ‘DefaultSlot’ 슬롯 메뉴를 찾아 추가하고 연결한다.
그 후 ABCharacter.cpp에서 만들어둔 Attack 함수 내에 로직을 추가한다.
여기까지 실행하면 한번의 마우스 왼쪽 클릭으로 캐릭터가 4번의 공격 애니메이션으로 구성된 몽타주 섹션이
끝날 때까지 실행된다.
// ABCharacter.cpp
#include "ABAnimInstance.h"
// 생략..
void AABCharacter::Attack()
{
auto AnimInstance = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());
if (nullptr == AnimInstance) return;
AnimInstance->PlayAttackMontage();
}
델리게이트는 넓은 의미로 본다면 특정 객체가 해야 할 로직을 다른 객체가 대신 처리할 수 있도록 만드는 보편적인 설계의 개념을 의미한다. 언리얼 엔진의 델리게이트는 A객체가 B객체에 작업 명령을 내릴 때 B객체에 자신을 등록하고 B의 작업이 끝나면 이때 A에게 알려주는
설계 방식을 의미한다.
C#에서는 기본으로 제공하지만 C++에서는 제공하지 않기에 언리얼 엔진에서 별도로 구축한 프레임워크를 사용하는 것이다.
어떤 객체가 가진 멤버 함수와 델리게이트를 연결하여 느슨한 결합을 만들 수 있다.
델리게이트 오브젝트를 복사해도 안전하지 힙에 메모리를 할당해야 하기 때문에 참조 전달을 권장함.
C++객체에만 사용할 수 있는 델리게이트와 C++객체, 블루프린트 객체 모두가 사용 가능한 델리게이트로 나뉨. 블루프린트 오브젝트는
멤버 함수에 대한 정보를 저장하고 로딩하는 직렬화 메커니즘이 들어있기 때문에 일반 C++ 언어가 관리하는 방법으로는 멤버 함수를 관리할 수가 없는 이유 때문이다. 그래서 블루프린트와 관련된 C++ 함수는 모두 UFUNCTION 매크로를 사용해야 한다. 이렇게 블루프린트 객체와도 연동하는 델리게이트를 다이나믹 델리게이트라고 한다.
애님 인스턴스에는 애니메이션 몽타주 재생이 끝나면 발동하는 OnMontageEnded라는 델리게이트를 제공한다.
어떤 언리얼 오브젝트라도 UAnimMontage* 인자와 bool 인자를 가진 멤버 함수를 가지고 있다면, 이를 OnMontageEnded 델리게이트에
등록해 몽타주 재생이 끝나는 타이밍을 파악할 수 있다.
또한 OnMontageEnded 델리게이트는 블루프린트와 호환되는 성질 되에도 여러 개의 함수를 받을 수 있어서 행동이 끝나면 등록된
모든 함수들에게 모두 알려주는 기능도 제공한다. 이러한 델리게이트를 멀티캐스트 델리게이트라고 한다.
애님 인스턴스 헤더에 선언된 OnMontageEnded가 사용하는 델리게이트를 정의한 코드는 다음과 같다.
언리얼 엔진에서 델리게이트의 선언은 언리얼이 제공하는 매크로를 통해 정의되며, 이렇게 정의된 델리게이트 형식을 시그니처라고 한다.
위의 선언 중 TwoParams와 같이 두 가지 기능이 있는 OnMontageEnded 델리게이트는 다이나믹 멀티캐스트 델리게이트라고 할 수 있다.
다이나믹 멀티캐스트 델리게이트에서 사용하는 AddDynamic 함수는 코딩할 때 비주얼 C++ 인텔리센스에 검색되지 않는다.
인텔리센스를 무시하고 타이핑해도 컴파일에는 문제가 되지 않는다.
애니메이션을 재생하는 동안 특정 타이밍에 애님 인스턴스에게 신호를 보내는 애니메이션 노티파이라는 기능을 제공한다.
애니메이션 노티파이는 일반 애니메이션과 몽타주 모두 사용 가능하다.
몽타주의 노티파이 부분에서 우클릭 → 노티파이 추가 → 새 노티파이로 추가한다.
이제 시스템에서 해당 몽타주 애니메이션을 재생하면 재생 구간에 위치한 노티파이를 호출하게 되고, 노티파이가 호출되면 언리얼 엔진은
이를 보고 자동으로 애님 인스턴스 클래스의 ‘AnimNotify_노티파이명’이라는 이름의 멤버 함수를 찾아서 호출한다.
이때 해당 멤버 함수는 언리얼 런타임이 찾을 수 있도록 반드시 UFUNCTION 매크로가 지정돼야 한다. 이는 앞서 이야기한 다이나믹 델리게이트와 연동하는 함수에 UFUNCTION을 붙여야 하는 이유와 동일하다. AnimNotify_AttackHitCheck라는 함수를 만들어 노티파이마다 로그를 찍게 해준다. 그 후 애니메이션을 실행해보면 노티파이마다 로그가 호출됨을 확인할 수 있다.
지금은 공격 명령이 한번 호출되면 4단계의 모든 공격 애니메이션을 수행하므로 Attack1 하나로 설정했던 4단계 공격 애니메이션을
각자 섹션으로 나눠서 관리하도록 하겠다.
몽타주에서 각 애니메이션 사이사이에 새로운 섹션을 우클릭 → 새 몽타주 섹션으로 만들어서 적당히 드래그해서 배치해준다.
또한 우측의 몽타주 섹션 창에서 화살표로 연동되어 있는 기본값을 지우기 버튼을 눌러 사진과 같이 독립적으로 바꿔준다.
그 후 노티파이란 우측 화살표를 눌러 노티파이 트랙을 새로 추가하고 해당 트랙에 NextAttackCheck란 노티파이를 추가하여 공격 모션이
끝나고 다시 Idle 애니메이션으로 되돌아가기 전 타이밍을 적절히 설정해준다.
마지막 Attack4 섹션에는 더 이상 연계할 콤보가 없고 Idle로 돌아가기 때문에 설정하지 않는다.
그 후 해당 프레임에 즉각적으로 반응하는 방식인 Branching Point 값으로 틱 타입을 변경해준다.
기본값인 Queued는 비동기 방식으로 신호를 받게 돼서 적절한 타이밍에 신호를 받는 것을 놓치게 될 수 있다.
Queued값은 주로 타이밍에 민감하지 않은 사운드나 이펙트를 발생시킬 때 사용하는 것이 적합하다.
설정을 할 때는 노티파이들을 Ctrl키를 누른 채 클릭하여 선택해서 한번에 바꿔줄 수 있다.
애님 인스턴스 클래스에서 콤보 카운트를 전달받으면 해당 몽타주 섹션을 재생하도록 기능을 구현한다.
그리고 앞서 선언한 NextAttackCheck 애니메이션 노티파이가 발생할 때마다 ABCharacter에 이를 전달할 델리게이트를 선언하고
애니메이션 노티파이 함수에서 이를 호출한다.
이렇게 델리게이트 기능을 사용하면 애님 인스턴스는 자신의 델리게이트를 사용하는 객체가 어떤 것인지 몰라도 델리게이트에 연결된
함수만 호출하면 되므로, 다른 클래스와 연결되지 않는 의존성 없는 설계를 할 수 있다는 장점이 있다.
반환 값과 인자 값이 없는 함수 유형으로 델리게이트를 선언하되, 여러 개의 함수가 등록되도록 멀티캐스트로 선언해본다.
(아래 DECLARE_MULTICAST_DELEGATE 부분)
멀티캐스트 델리게이트에 등록된 모든 함수를 호출하는 명령은 Broadcast다.
C++ 11 규약부터 정식으로 추가된 람다식은 함수를 헤더에 선언할 필요가 없으므로 간단한 로직을 간편하게 처리할 수 있는 장점이 있다.
람다식은 세 가지 영역으로 구분된다.
- 람다 소개자(Lambda Introducer): []로 표시되며 람다 구문이 참조할 환경을 지정한다. 람다 함수가 참조할 환경을 캡처라고도 한다.
- 파라미터 리스트(Parameter List): 람다 함수가 사용할 파라미터를 지정하는 구문이다. 우리가 사용할 델리게이트는 함수 인자가 없으므로 빈 괄호를 사용한다.
- 후행 반환 타입(Trailling Return Type): ‘→’ 기호를 사용한 후 람다 함수가 반환할 타입을 지정한다.
- 람다 함수 구문(Lambda Body): {}로 캡처 환경을 사용한 람다 함수의 로직을 넣어준다.
'개발 공부 기록 > UnrealEngine5' 카테고리의 다른 글
이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 8 (0) | 2024.11.21 |
---|---|
이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 7 (1) | 2024.11.21 |
이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 5 (1) | 2024.11.07 |
이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 4 (3) | 2024.11.06 |
이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 3 (0) | 2024.11.02 |