이득우의 언리얼 C++ 게임 개발의 정석을 읽고 개인 공부 목적으로 요약 정리한 글입니다!
특정 상황에서 원하는 애니메이션을 재생할 수 있게 해주는 기능
스테이트 머신과는 독립적이다
(몽타주)
몽타주는 섹션(Section) 단위로 애니메이션을 관리함
여러 애니메이션 클립을 자르고 붙여서 원하는 새로운 애니메이션을 만들어낼 수 있다.
위 사진의 예시에서는,
Attack 1 ~ Attack 5의 섹션으로 구성했고,
각 섹션 별로 연동을 하지 않기 위해
몽타주 섹션에서 지우기를 눌렀다!
이제 각 섹션은 독립적으로 실행된다.
몽타주를 재생하려면 애니메이션 블루프린트의 애님 그래프에 추가해줘야 한당.
기본 IDLE, RUN, JUMP 와는 독립적으로 모든 상황에서 공격 애니메이션을 재생하고 싶으니까
스테이트 머신과 최종 결과 사이에 넣어줬당.
특정 객체가 해야 할 로직을 다른 객체가 대신 처리할 수 있도록 만드는 보편적인 설계의 개념
A 객체가 B 객체에 작업 명령을 내릴 때, B 객체에 자신을 등록하고 B의 작업이 끝나면 A에게 알려준다.
c#은 기본적으로 제공하는데, c++은 별도록 구축한 델리게이트 프레임워크를 사용해야 한다.
약간 겜서버에서 배운 RPC 개념 느낌인건가 이 부분 확실한 이해 필요할듯
애님 인스턴스의 델리게이트
우리가 선언한 함수
두 개를 연결한다고 보면 된다.
애님 인스턴스에 OnMontageEnded 델리게이트를 만들고
MyCharacter에 OnAttackMontageEnded 함수를 만든다.
그리고 OnAttackMontageEnded 함수를 OnMontageEnded 델리게이트에 등록한다.
그러면 몽타주가 끝났을 때 OnAttackMontageEnded를 호출해준다.
블루프린트 객체와도 연동하는 델리게이트
C++ 객체에서만 사용할 수 있는 델리게이트도 있고,
C++/Blueprint 모두 사용할 수 있는 델리게이트도 있다.
블루프린트 오브젝트는 멤버함수에 대한 정보를 저장하고 로딩하는 직렬화 알고리즘이 있기 때문에
Blueprint와 관련된 C++ 함수는 UFUNCTION 매크로를 사용해야 한다.
델리게이트의 기능 중 하나인데,
블루프린트와 호환되는 성질 이외에도,
여러 개의 함수를 받아서 행동이 끝나면 등록된 모든 함수들에게 알려주는 기능을 제공한다.
언리얼이 제공하는 매크로를 통해 정의된 델리게이트의 형식
언리얼에서는 델리게이트를 선언할 때 위에 처럼 매크로를 통해 정의해야 한다.
다이내믹 + 멀티캐스트
합쳐질 수도 있다.
DECLARE_DYNAMIC_DELEGATE_TwoParams(FOnMontageEndedMCDelegate, UAnimMontage* Montage, bool, bInterrupted);
이렇게,, 두 가지 기능을 가지면 다이내믹 멀티캐스트 델리게이트다
: 애니메이션을 재생하는 동안 특정 타이밍에 애님 인스턴스에게 신호를 보내는 기능.
이 것이 애니메이션 노티파이 설정한 모습.
앞 사진과 똑같이 Attack1~Attack5가 각각 독립적인 섹션으로 구성되어 있다.
이 상황에서 공격이 들어갔는지 여부를 체크하고, 다음 공격의 여부를 위해 애님 노티파이를 넣었다.
애니메이션 재생 중 공격버튼을 또 누르면 다음 섹션이 재생되도록 하겠다는 거다.
그래서
AttackHitCheck : 현재 공격이 들어갔는지 체크
NextAttackCheck : 다음 공격이 들어갈 예정인지 체크
이렇게 노티파이가 들어가면,
몽타주 애니메이션을 재생하면 재생 구간에 위치한 노티파이를 호출해준다.
내가 아니라 언리얼이~!!!
노티파이가 호출되면 자동으로 애님 인스턴스의 AnimNotify_노티파이명 함수를 호출한다.
이 멤버함수는 언리얼 런타임이 찾아야 하므로 UFUNCTION 매크로가 붙어야 한다!
Category >> 이벤트 >> 몽타주 틱 타입을 Branching Point로 설정하자.
왜냐하면 Branching Point가 해당 프레임에 즉각적으로 반응한다.
Queued는 비동기 방식으로 신호를 받기 때문에, 타이밍을 놓칠 수 있다.
보통 Queued는 사운드나 이펙트를 발생시킬때 사용하면 된다!
class HUNT_PROTOTYPE_API AMyCharacter : public ACharacter
{
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;
...
private:
UFUNCTION()
void OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);
void AttackStartComboState();
void AttackEndComboState();
...
};
CanNextCombo : 다음 콤보로 이동 가능 여부
IsComboInputOn : 콤보 공격 입력 여부
CurrentCombo : 현재 콤보 cnt
MaxCombo : 최대 콤보 cnt
AMyCharacter::AMyCharacter()
{
...
IsAttacking = false;
MaxCombo = 4;
AttackEndComboState();
}
void AMyCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
MyAnim = Cast<UMyAnimInstance>(GetMesh()->GetAnimInstance());
HUNT_CHECK(nullptr != MyAnim);
MyAnim->OnMontageEnded.AddDynamic(this, &AMyCharacter::OnAttackMontageEnded);
MyAnim->OnNextAttackCheck.AddLambda([this]() -> void {
HUNT_LOG(Warning, TEXT("OnNextAttackCheck"));
CanNextCombo = false;
if (IsComboInputOn) {
AttackStartComboState();
MyAnim->JumpToAttackMontageSection(CurrentCombo);
}
});
}
void AMyCharacter::Attack()
{
if (IsAttacking) {
HUNT_CHECK(FMath::IsWithinInclusive<int32>(CurrentCombo, 1, MaxCombo));
if (CanNextCombo) {
IsComboInputOn = true;
}
}
else {
HUNT_CHECK(CurrentCombo == 0);
AttackStartComboState();
MyAnim->PlayAttackMontage();
MyAnim->JumpToAttackMontageSection(CurrentCombo);
IsAttacking = true;
}
}
void AMyCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
HUNT_CHECK(IsAttacking);
HUNT_CHECK(CurrentCombo > 0);
IsAttacking = false;
AttackEndComboState();
}
void AMyCharacter::AttackStartComboState()
{
CanNextCombo = true;
IsComboInputOn = false;
HUNT_CHECK(FMath::IsWithinInclusive<int32>(CurrentCombo, 0, MaxCombo - 1));
CurrentCombo = FMath::Clamp<int32>(CurrentCombo + 1, 1, MaxCombo);
}
void AMyCharacter::AttackEndComboState()
{
IsComboInputOn = false;
CanNextCombo = false;
CurrentCombo = 0;
}
AttackStartComboState()
, AttackEndComboState()
를 통해 공격이 시작하고 끝날 때 호출할 함수를 만들었다.
Attack()
에서 초기 공격인지, 콤보 진행중인지 판단하고 몽타주를 재생, 콤보 관리를 해준다.
PostInitializeComponents()
에서는,
NextAttackCheck 노티파이가 발생하면 호출해줄 함수를 람다를 통해 선언했다.
NextAttackCheck 노티파이가 발생했는데 콤보 입력이 들어왔다?
바로 다음 콤보 섹션을 재생하도록 해준다.
FMath::Clamp
(x, min, max)
x값을 min과 max의 사이로 유지시켜 준다.
값의 범위를 지정해준다고 하면 될듯.
FMath::IsWithinInclusive
: 값이 범위 내부에 있는지 검사해준다.
람다 (Lambda)
함수를 헤더에 선언할 필요 없이 사용할 수 있다!!
[](){};
형식으로 사용한다.
[]
: 캡쳐
람다 구문이 참조할 환경
현재는 인스턴스의 관련 멤버 변수와 멤버 함수를 사용할거니까 this로 한다.
외부 변수나 함수를 사용하고 싶으면, 여기다 넣을 수 있는데
복사, 참조 모두 가능하다int a1; int a2; [a1, a2](){ ... }; [&a1, &a2](){ ... };
모두 가능하다는 뜻
()
: 인자 (파라미터) 지정
람다 함수가 사용할 파라미터를 지정할 수 있다.
{}
: 함수 구문
람다 함수의 로직이 작성되는 부분.
[]로 지정한 캡쳐 환경에서 변수와 함수들을 사용하여 로직을 짜면 된다.
DECLARE_MULTICAST_DELEGATE(FOnNextAttackCheckDelegate);
DECLARE_MULTICAST_DELEGATE(FOnAttackHitCheckDelegate);
class HUNT_PROTOTYPE_API UMyAnimInstance : public UAnimInstance
{
private:
...
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true));
UAnimMontage* AttackMontage;
public:
....
void JumpToAttackMontageSection(int32 NewSection);
public:
FOnNextAttackCheckDelegate OnNextAttackCheck;
FOnAttackHitCheckDelegate OnAttackHitCheck;
private:
UFUNCTION()
void AnimNotify_AttackHitCheck();
UFUNCTION()
void AnimNotify_NextAttackCheck();
FName GetAttackMontageSectionName(int32 Section);
};
앞서 말했듯이,
멀티캐스트 델리게이트를 매크로를 통해 정의해줬다.
void UMyAnimInstance::JumpToAttackMontageSection(int32 NewSection)
{
HUNT_CHECK(Montage_IsPlaying(AttackMontage));
Montage_JumpToSection(GetAttackMontageSectionName(NewSection), AttackMontage);
}
void UMyAnimInstance::AnimNotify_AttackHitCheck()
{
OnAttackHitCheck.Broadcast();
}
void UMyAnimInstance::AnimNotify_NextAttackCheck()
{
OnNextAttackCheck.Broadcast();
}
FName UMyAnimInstance::GetAttackMontageSectionName(int32 Section)
{
HUNT_CHECK(FMath::IsWithinInclusive<int32>(Section, 1, 4), NAME_None);
return FName(*FString::Printf(TEXT("Attack%d"), Section));
}
BroadCast()
: 델리게이트에 등록된 모든 함수를 호출해준다.
몽타주가 재생되다가
애님 노티파이 부분이 되면
AnimNotify_NextAttackCheck를 호출해준다.
그러면 함수 내부로 들어오겠지?
BroadCast를 통해 OnNextAttackCheck (Delegate 멤버 변수)에 등록된 함수들을 호출해준다.
그래서 AMyCharacter.cpp에서 선언한 람다 함수가 불리는거임
그래서 람다함수에서 콤보 가능이면 JumpToAttackMontageSection()을 호출해서 다음 섹션으로 넘어가게 한다.
처음 했는데 계속 다음 콤보로 안넘어가는거다...
NextAttackCheck가 섹션의 중간쯤에는 있어야 한다.
너무 뒤에 있으면 다음 섹션을 호출하기 전에 OnMontageEnded가 호출되어서
콤보 공격이 실행이 안된다.
그래서 애먹었다..
처음에 계속 첫번째것만 하고 로그는 찍히는데
Montage_IsPlay에서 오류가 뜨는고임
근데 알고보니 너무 뒤에 놔서 그런거였음..