애니메이션 몽타주, 노티파이라는 기능을 활용하여
연속으로 공격 명령을 내리면 콤보 공격을 순서대로 연계하는,
만약 입력이 늦으면 다시 처음부터 공격을 시작하는 콤보 시스템을 구현해 보자
<1>. 애니메이션 몽타주
위에서 말한 콤보 시스템은 지난 장에서 배운 스테이트 머신을 사용해서 모든 공격에 대해 스테이트를 생성하면 구현할 수 있다. 하지만 이렇게 되면 너무 복잡해진다.
스테이트 머신의 확장 없이 특정 상황에서 원하는 애니메이션을 발동시키는 '애니메이션 몽타주'라는 기능이 있다.
※애니메이션 몽타주 : 여러 애니메이션 클립들의 일부를 떼어내고 붙여서 새로운 애니메이션을 생성하는 기법
'애님 몽타주' 생성 후, WarriorAttack1, 2, 3, 4를 드래그로 등록하고 각 모션이 끝나는 시각을 파악하여 '끝 시간'을 조정하여 연결된 콤보 공격 모션을 생성한다. → Action Mappings에서 마우스 좌클릭을 누르면 콤보 공격을 하게끔 한다.
몽타주 에셋과 관련된 명령은 항상 몽타주 에셋을 참조하기 때문에, 멤버함수와 변수를 생성해서 변수에 미리 저장해 주면 편리하다.
//ABAnimInstance.h
UCLASS()
{
public:
void PlayAttackMontage();
...
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category=Attack,
Meta=(AllowPrivateAccess=true))
UAnimMontage* AttackMontage;
}
//ABAnimInstance.cpp
UABAnimInstance::UABAnimInstance()
{
static ConstructorHelpers::FObjectFinder<UAnimMontage>ATTACK_MONTAGE(TEXT("/Game/.../(몽타주 에셋 경로)"));
if(ATTACK_MONTAGE.Succeeded())
{
AttackMontage = ATTACK_MONTAGE.Object;
}
}
void UABAnimInstance::PlayAttackMontage()
{
if(!Montage_IsPlaying(AttackMontage))
{
Montage_Play(AttackMontage, 1.0f);
}
}
애니메이션 블루프린트에서 이를 재생하려면 몽타주 재생 노드를 애님 그래프에 추가해야 한다.
우리는 모든 상황에서 몽타주를 재생할 예정이므로, '최종 애니메이션 포즈'와 '스테이트 머신' 사이에 몽타주 재생 노드를 추가한다. (DefaultSlot 노드 사용)
//ABCharacter.cpp
#include "ABAnimInstance.h"
void AABCharacter::Attack()
{
auto AnimInstance = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());
if(nullptr == AnimInstance) return ;
AnimInstance->PlayAttackMontage();
}
<2>. 델리게이트
'Montage_IsPlaying' 함수를 사용하여 시스템이 계속 구동 중인지 체크하는 방식보다는
→ 몽타주 재생이 끝나면 공격이 가능하다고 폰에게 알려주는 방식이 더 효과적이다.
※델리게이트
애님 인스턴스에는 애니메이션 몽타주 재생이 끝나면 발동하는 'OnMontageEnded'라는 델리게이트를 제공한다.
'UAnimMontage *' 인자와 'bool' 인자를 가진 멤버 함수가 있다면 OnMontageEnded 델리게이트에 등록해서 몽타주 재생이 끝나는 타이밍을 파악할 수 있다.
//ABCharacter.h
UCLASS()
{
...
public:
virtual void PostInitializeComponents() override;
//특정 이벤트가 게임 시작 시점부터 동작해야 한다면
//PostInitializeComponents()에 바인딩하는게 적절하다.
...
private:
UFUNCTION() //블루프린트와 호환되는 함수형으로 선언
void OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);
//UAnimMontage인자와 bool인자를 가진 멤버함수
private:
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category=Attack,
Meta=(AllowPrivateAccess=true))
bool IsAttacking;
};
OnMontageEnded 델리게이트와 우리가 선언한 'OnAttackMontageEnded'를 연결하여 공격할 동안 몽타주 재생 명령을 내리지 못하게 폰 로직에서 막아주자.
//ABCharacter.cpp
AABCharacter::AABCharacter()
{
IsAttacking = false; //초기화
}
void AABCharacter::PostInitialize()
{
Super::PostInitializeComponents();
auto AnimInstance = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());
AnimInstance->OnMontageEnded.AddDynamic(this, &AABCharacter::OnAttackMontageEnded);
//델리게이트와 멤버함수 바인딩하기
}
void AABCharacter::Attack()
{
if(IsAttacking) return;
...
IsAttacking = true;
}
void AABCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
IsAttacking = false;
}
캐릭터 클래스에서 애님 인스턴스를 자주 사용할 예정이므로, 이를 멤버 변수로 전방 선언하자.
//ABCharacter.h
UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
...
private:
UPROPERTY()
class UABAnimInstance* ABAnim; // AnimInstance.h 를 include하는게 아니고
}; // 멤버함수를 전방선언 해준다
//ABCharacter.cpp
void AABCharacter::PostInitializeComponents()
{
ABAnim = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());
ABAnim->OnMontageEnded.AddDynamic(this, &AABCharacter::OnAttackMontageEnded);
}
void AABCharacter::Attack()
{
ABAnim->PlayAttackMontage();
}
<3>. 애니메이션 노티파이
※애니메이션 노티파이 : 애니메이션 재생동안 '특정 타이밍'에 애님 인스턴스에게 '신호를 보내는' 기능
공격하는 타이밍에 공격 판정 신호를 보내게끔 해보자.
→ 팔을 뻗는 공격 타이밍에 맞춰서 'AttackHitCheck'라는 노티파이를 배치한다.
→ 노티파이가 호출되면 'AnimNotify_노티파이명' 함수가 자동으로 호출된다.
//ABAnimInstance.h
UCLASS()
{
private:
UFUNCTION() //언리얼 런타임이 찾을 수 있도록
void AnimNotify_AttackHitCheck();
...
}
//ABAnimInstance.cpp
void UABAnimInstance::AnimNotify_AttackHitCheck()
{
ABLOG_S(Warning);
}
<4>. 콤보 공격의 구현
1. 각 공격 동작을 섹션으로 분리한 후, 일정 시간 내에 공격 명령을 내리면 다음 동작으로 이동하는 콤보 공격을 구현해 보자.
→ 공격 모션이 끝나고 다시 IDLE로 되돌아가기 전에 'NextAttackCheck'라는 새로운 애니메이션 노티파이 배치.
→ 노티파이의 '틱 타입' Queued(디폴트값)에서 Breaching Point로 변경. (프레임에 즉각적으로 반응함)
2. ABCharacter에 콤보 단계의 카운터, 현재 콤보 카운터, 다음 콤보 가능 여부, 콤보 입력 여부를 변수로 선언한다.
3. 공격 시작할 때의 관련 속성, 공격 종료할 때의 관련 속성 함수 선언, 구현
//ABCharacter.h
//2, 3번 과정 선언 생략
//ABCharacter.cpp
AABCharacter::AABCharacter()
{
...
MaxCombo = 4;
AttackStartComboState();
AttackEndComboState();
}
void AABCharacter::AttackStartComboState() //공격 시작할 때의 속성 설정
{
CanNextCombo = true;
IsComboInputOn = false;
CurrentCombo = FMath::Clamp<int32>(CurrentCombo + 1, 1, MaxCombo);
}
void AABCharacter::AttackEndComboState() //공격 끝날 때의 속성 설정
{
IsComboInputOn = false;
CanNextCombo = false;
CurrentCombo = 0;
}
이제 콤보 카운터(1, 2, 3, 4)를 전달받으면 해당 몽타주 섹션을 재생하도록 기능을 구현해 보자.
NextAttackCheck 노티파이가 발생할 때마다 ABCharacter에 이를 전달할 멀티캐스트 델리게이트를 선언하고 노티파이 함수에서 이를 호출한다.
※ 멀티캐스트 델리게이트 : 한 개 이상의 함수를 호출할 수 있는 델리게이트(호출명령 = Broadcast)
//ABAnimInstance.h
DECLARE_MULTICAST_DELEGATE(FOnNextAttackCheckDelegate); //멀티캐스트 델리게이트 선언
DECLARE_MULTICAST_DELEGATE(FOnAttackHitCheckDelegate);
class ARENABATTLE_API UABAnimInstance:public UAnimInstance
{
...
public:
void JumpToAttackMontageSection(int32 NewSection); //콤보 카운트 받아서 해당 몽타주 섹션 재생
public:
FOnNextAttackCheckDelegate OnNextAttackCheck; //멀티캐스트 델리게이트 인스턴스 선언
FOnAttackHitCheckDelegate OnAttackHitCheck;
private:
UFUNCTION()
void AnimNotify_NextAttackCheck(); //해당 몽타주 재생되면 자동으로 호출됨
FName GetAttackMontageSectionName(int32 Section);
};
//ABAnimInstance.cpp
void UABAnimInstance::JumpToAttackMontageSection(int32 NewSection)
{
Montage_JumpToSection(GetAttackMontageSectionName(NewSection), AttackMontage);
//구한 섹션으로 이동
}
void UABAnimInstance::AnimNotify_AttackHitCheck()
{
OnAttackHitCheck.Broadcast(); //멀티태스크 델리게이트 호출
}
void UABAnimInstance::AnimNotify_NextAttackCheck()
{
OnNextAttackCheck.Broadcast();
}
FName UABAnimInstance::GetAttackMontageSectionName(int32 Section)
{
return FName(*FString::Printf(TEXT("Attack%d"), Section));
}
1. 공격 명령을 내리면 ABCharacter는 콤보가 가능한지 아닌지 파악하기.
2. NextAttackCheck 타이밍 전까지 공격 명령 들어오면 다음 콤보 시작. 이를 파악하기 위해 CanNextCombo속성 사용.
3. OnNextAttackCheck 델리게이트와 등록할 로직 선언, 구현(람다식으로)
//ABCharacter.cpp
void AABCharacter::PostInitializeComponents()
{
ABAnim = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());
ABAnim->OnMontageEnded.AddDynamic(this, &AABCharacter::OnAttackMontageEnded);
//OnMOntageEnded 가 종료될 때, OnAttackMontageEnded 호출
ABAnim->OnNextAttackCheck.AddLambda([this]()->void{
CanNextCombo = false;
if(IsComboInputOn)
{
AttackStartComboState();
ABAnim->JumpToAttackMontageSection(CurrentCombo);
}
});
}
void AABCharacter::Attack()
{
if(IsAttacking)
{
if(CanNextCombo)
{
IsComboInputOn = true;
}
}
else
{
AttackStartComboState(); // 콤보시작 속성 설정
ABAnim->PlayAttackMontage(); // 몽타주 재생
ABAnim->JumpToAtackMontageSection(CurrentCombo); // 다음 섹션으로 점프
IsAttacking = true;
}
}
void AABCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
IsAttacking = false;
AttackEndComboState();
}
'게임 개발 > 이득우의 언리얼 C++' 카테고리의 다른 글
[이득우의 언리얼 C++ 게임개발의 정석] #10. 아이템 상자와 무기 제작 (0) | 2024.06.25 |
---|---|
[이득우의 언리얼 C++ 게임개발의 정석] #9. 충돌 설정과 대미지 전달 (0) | 2024.06.22 |
[이득우의 언리얼 C++ 게임개발의 정석] #7. 애니메이션 시스템의 설계 (0) | 2024.06.17 |
[이득우의 언리얼 C++ 게임개발의 정석] #6. 캐릭터의 제작과 컨트롤 (0) | 2024.06.13 |
[이득우의 언리얼 C++ 게임개발의 정석] #5. 폰의 제작과 조작 (0) | 2024.06.10 |