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을 차례대로 보여주는 것을 알 수 있다.