
Delegate에 대해 선행 공부를 하고 싶다면 이 링크를 참고하자

여러개의 animation clip을 모아둔 다수의 section으로 구성. (중요 !!) AnimInstance 함수를 통해서 원하는 section으로 건너뛰게 할 수 있다.
GetMesh() -> GetAnimInstance() -> Montage_Play(몽타주 애셋)으로 특정 Montage를 재생할 수 있다. 
각 콤보마다 입력을 테스트하는 frame을 지정
지정된 frame 이전에 입력이 들어오면 자동으로 다음 montage section 플레이하게 연결
// ComboActionData.h
...
// 몽타주 section 이름
UPROPERTY(EditAnywhere, Category = Name)
FString MontageSectionNamePrefix;
// 총 몇개의 combo가 존재하는지
UPROPERTY(EditAnywhere, Category = Name)
uint8 MaxComboCount;
UPROPERTY(EditAnywhere, Category = Name)
float FrameRate;
UPROPERTY(EditAnywhere, Category = ComboData)
TArray<float> EffectiveFrameCount;

처음 시작은 Character.cpp부분에 Attack함수를 만들고 이를 EnhancedInputComponent에 binding한다.
EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &AMyRyanCharacter::Attack);
Attack함수는 ProcessComboCommand라는 함수를 부르게 되고 여기서부터는 CharacterBase.cpp에서 모든 로직이 처리되게 된다.
// 처음(CurrentCombo가 0)에만 시작 함수를 따로 구현하고
// 나머지에 대해서는 timer로 처리하게 한다.
// 타이머가 돌면서 HasNextComboCommand값을 확인하는 방식.
void ARyanCharacterBase::ProcessComboCommand()
{
if (CurrentCombo == 0)
{
ComboActionBegin();
return;
}
if (!ComboTimerHandle.IsValid())
{
HasNextComboCommand = false;
}
else
{
HasNextComboCommand = true;
}
}
ProcessComboCommand는 CharacterBase에서 처음 Attack 관련해서 시작되는 함수이다. 처음에 키를 눌렀을때(CurrentCombo가 0)와 아닌 상태의 로직이 다르다. 0일때는 ComboActionBegin()을 실행하고 아닐때는 HasNextComboCommand의 bool 값만 조절하고 Timer가 expire되기 전에 이 값을 체크하면서 콤보 처리를 구현한다.
void ARyanCharacterBase::ComboActionBegin()
{
// Combo Status
CurrentCombo = 1;
// Movement Setting
// MOVE_None으로 setting하면 캐릭터의 이동기능이 상실된다.
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
// Animation Setting
const float AttackSpeedRate = 1.0f;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->Montage_Play(ComboActionMontage, AttackSpeedRate);
// Montage_SetEndDelegate에 ComboActionEnd라는 함수를 등록하고 Montage가 끝날때 자동으로 실행될 수 있게 한다.
FOnMontageEnded EndDelegate;
EndDelegate.BindUObject(this, &ARyanCharacterBase::ComboActionEnd);
AnimInstance->Montage_SetEndDelegate(EndDelegate, ComboActionMontage);
ComboTimerHandle.Invalidate();
SetComboCheckTimer();
}
void ARyanCharacterBase::ComboActionEnd(UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
ensure(CurrentCombo != 0);
CurrentCombo = 0;
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}
먼저 처음 콤보가 시작될때 어떻게 되는지를 확인해보자. ComboActionBegin 함수에서는 skeletal mesh에서 AnimInstance를 가져오고, 이를 통해 첫번째 Attack Montage section을 play한다. 그리고 Montage가 종료될때 사용하라고 만든 FOnMontageEnded 구조체를 선언하고, Montage_SetEndDelegate 델리게이트에 ComboActionEnd라는 함수를 등록한다. 이 함수를 Montage가 최종적으로 끝날때 실행되게 된다. 델리게이트에 바인딩 된 ComboActionEnd 함수는 단순히 Montage가 끝났을때 Combo 값을 0으로 돌려놓고 Character Movement를 해제시키는 함수이다.
void ARyanCharacterBase::SetComboCheckTimer()
{
int32 ComboIndex = CurrentCombo - 1;
//FString FormattedString = FString::Printf(TEXT("ComboIndex: %d, CurrentCombo: %d"), ComboIndex, CurrentCombo);
//GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Green, FormattedString);
ensure(ComboActionData->EffectiveFrameCount.IsValidIndex(ComboIndex));
const float AttackSpeedRate = 1.0f;
// frame수를 frame rate로 나누면 play된 시간이 나오게 된다.
// EffectiveFrameCount 배열에는 AttackMontage의 각 section 공격에 대한 frame 구간이 있다. [17,17,20,0] 이렇게 구성되어 있음.
// ComboTimerHandle이라는 타이머를 만들어서 ComboEffectiveTime 후에 ComboCheck이라는 함수를 call하게 한다.
float ComboEffectiveTime = (ComboActionData->EffectiveFrameCount[ComboIndex] / ComboActionData->FrameRate) / AttackSpeedRate;
if (ComboEffectiveTime > 0.0f)
{
GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &ARyanCharacterBase::ComboCheck, ComboEffectiveTime, false);
}
}
SetComboCheckTimer는 ComboTimerHandle을 통해 언리얼 Timer 시스템을 만들어 주는 함수이다. 이 타이머는 우리가 사전에 설정한 DataAsset에서 Montage의 유효클릭이 가능한 프레임을 ComboEffectiveTime라는 변수에 저장한다. 타이머는 ComboEffectiveTime의 시간이 지난 후에 ComboCheck이라는 함수를 call하고 사라진다.
void ARyanCharacterBase::ComboCheck()
{
ComboTimerHandle.Invalidate();
// 만약 정해진 시간안에 다시 attack key를 눌렀다면 HasNextComboCommand가 true로 설정되어 있을것.
if (HasNextComboCommand)
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
// 지정한 콤보 index값을 넘어가면 안되기 때문에 Clamp로
// 여기서 CurrentCombo 값을 업데이트한다.
CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, ComboActionData->MaxComboCount);
// Montage에서 이동할 section 이름값 구하기.
// FString::Printf로 string concatenation을 한다.
FName NextSection = *FString::Printf(TEXT("%s%d"), *ComboActionData->MontageSectionNamePrefix, CurrentCombo);
AnimInstance->Montage_JumpToSection(NextSection, ComboActionMontage);
//다음 montage를 play하므로 타이머는 다시 초기화
SetComboCheckTimer();
HasNextComboCommand = false;
}
}
마지막 부분을 보면, 처음에 ProcessComboCommand()의 아리송 했던 부분을 이해할 수 있다. ComboCheck함수는 HasNextComboCommand라는 변수값을 체크하면서 true일때만 동작한다. 위에서 타이머가 만료되려고 할때 HasNextComboCommand 값이 true이면 Montage_JumpToSection을 통해 다음 Montage를 재생하고 다시 SetComboCheckTimer을 불러 timer를 초기화시킨다. 만약 HasNextComboCommand 값이 false라면, 타이머를 invalidate 시킨다.
복잡한 내용을 정리하자면, 처음에 Attack키를 눌렀을때 ProcessComboCommand 함수가 호출되게 되는데 여기서 CurrentCombo가 0일때와 아닐때 로직을 나누어서 본다. 0일때는 직관적으로 ComboActionBegin이라는 함수를 실행하고 첫번째 Montage section을 play한다. 여기서 ComboTimerHandle라는 Timer을 작동시키게 된다.
이 Timer은 자기에게 생성될때 주어진 시간이 지나면 ComboCheck라는 함수를 call하고 expire가 되게 되는데, 이때 HasNextComboCommand라는 변수를 확인해보게 된다. 이 변수는 사용자가 combo에 알맞는 시간에 다시 attack 키를 누르면 true로 바뀌게 되고 아니면 false로 값이 설정된다. 이렇게 처음 키를 누르는 경우를 제외하고는 다 timer 방식으로 다음 montage section을 play하여 attack combo를 구현하게 된다.
##############################################
AnimInstance->함수()식으로 사용
##############################################

타이머가 expire되기 전에 적절한 타이밍에 attack 버튼을 누르면 Animation Montage에서 Attack1, Attack2, Attack3, Attack4에 해당하는 animation section을 차례대로 보여주는 것을 알 수 있다.