개요
원래는 모든 구현 과정을 블로그로 정리하려고 했지만, 10.08일 최종 발표이며, 아직 구현하지 못한 기능이 많고 빠르게 기능 구현을 진행하다 보니 이미 정리했던 부분과의 차이가 너무 벌어지게 되었습니다.
그래서 이번 포스팅에서는 콤보 공격의 구현 아이디어와 로직을 중점으로 정리해보도록 하겠습니다.
콤보 공격 구현 아이디어
콤보 공격은 AnimMontage를 사용해 구현할 수 있습니다.
우선 여러 ComboAttack 애니메이션을 구한 뒤 하나의 몽타주에서 섹션을 나눠 섹션간의 연결을 끊어주도록 합니다.
위와 같이 구성하게 되면 Combo1의 공격 모션이 종료되면 콤보 공격을 위해 JumpToSection() 함수를 사용해 Combo2로 넘기는 방식으로 콤보를 자연스럽게 이어갈 수 있습니다.
제가 구현해본 방식은 크게 2가지 입니다.
- 애니메이션 노티파이를 사용하는 방식
- 다음 콤보로 이어지는 지점을 체크하기 위한 애님 노티파이를 몽타주에 추가하여 해당 이벤트 발생 시 추가 공격 입력이 들어왔는지 체크하는 방식입니다.
- 추가 공격 입력이 해당 노티파이 전까지 들어온 경우 다음 Section으로 Jump To Section()을 수행합니다.
- 추가 공격 입력이 들어오지 않았다면 해당 Secion이 종료되는 순간 몽타주 재생이 끝나게 됩니다.
- 데이터 애셋을 활용해 콤보 시간을 체크하는 방식
- 간단히 기존에 사용했던 노티파이를 추가하는 대신, 해당 시간을 저장하는 방식이라고 생각하면 됩니다.
- 노티파이를 사용하는 방식과 로직은 동일하지만, 여러 종류의 콤보 공격을 구현할 때 편리합니다.
저의 경우 구현해야 하는 콤보 공격이 4종류이기에 쉽게 확장 가능한 데이터 애셋 방식을 사용하게 되었습니다.
콤보 공격 데이터 제작
위 사진에서 본 콤보 공격 데이터 애셋을 생성해보도록 하겠습니다. (C++)
우선 UDataAsset을 상속받은 데이터 애셋 클래스를 제작해주도록 합니다.
데이터 애셋 내부에 저장할 데이터는 마음대로 설정해도 되지만 저의 경우 몽타주부터 최대 콤보 수 등 콤보 공격에 필요한 모든 Default 데이터들을 관리할 수 있도록 구현하였습니다.
콤보 공격 로직 구현
우선 로직 구성에 필요한 변수들부터 선언해주도록 하겠습니다.
ComboAttackDatas
- 위에서 언급했던 대로 4종류의 무기별 콤보 공격이 필요하기에 데이터는 TMap<무기 타입, 콤보 공격 데이터> 형식으로 저장하였습니다.
ComboTimerHandle
- 콤보 공격 데이터의 ComboFrame[]을 사용해 타이머를 설정하여 타이머가 종료되고 바인딩된 함수가 호출되면 체크할 예정입니다. 타이머를 관리하기 위해 필요한 타이머 핸들을 선언하였습니다.
bIsAttacking
- 현재 공격중인지를 판별하기 위한 변수입니다. 현재 공격중인 경우 추가적인 공격 입력이 들어오면 콤보 입력 판별 변수(bIsComboInput)를 활성화(true)로 설정해주는 역할입니다.
bIsComboInput
- 콤보 입력이 들어왔는지 체크하는 변수입니다. 설정된 타이머가 완료되어 호출되는 체크 함수에서 해당 변수가 활성화(true) 되어있는 경우 다음 섹션으로 애님 몽타주를 넘겨주게 됩니다.
ComboCount
- 현재 진행중인 콤보의 수를 저장하기 위한 변수입니다. 해당 변수를 통해 인덱스를 계산하고, 현재 상황에서 올바른 ComboFrame[]을 호출하기 위해 사용됩니다.
다음으로는 콤보 로직에 대해 정리하도록 하겠습니다.
Attack() 함수는 공격 입력이 들어오면 실행되는 함수입니다.
- 현재 공격 중이 아니면 콤보 공격을 시작합니다. (ComboStart)
- 현재 공격 중이라면 콤보 입력 변수를 활성화합니다.
ComboStart() 함수는 콤보 공격의 첫 공격을 담당하는 함수입니다.
- 콤보 수를 1로 설정하고 콤보 데이터에 저장된 콤보 공격 몽타주를 재생합니다.
- 몽타주 종료 델리게이트에 종료되면 호출되는 함수(EndCombo)를 바인딩합니다.
- 이후 타이머를 초기화하고 타이머를 설정(SetComboTimer)해줍니다.
SetComboTimer() 함수는 콤보 공격을 체크하기 위한 타이머를 설정하는 함수입니다.
- 현재 콤보 수를 확인하여 콤보 공격 데이터의 실제 인덱스를 구해줍니다. (ComboIndex)
- 해당 인덱스가 유효한 경우 콤보 공격 데이터의 ComboFrame[ComboIndex]에 저장된 시간에 콤보 체크 함수(ComboCheck)가 호출되도록 타이머를 설정합니다.
CheckCombo() 함수는 해당 시점 이전에 콤보 입력이 추가적으로 들어왔는지 체크하기 위한 함수입니다.
- Attack() 함수에서 bIsAttacking이 true인 경우 추가적인 입력을 받으면 bIsComboInput 변수가 활성화됩니다.
- bIsComboInput가 활성화된 경우 현재 콤보의 수를 증가시켜주고 다음 섹션으로 넘어갈 수 있는지 체크합니다.
- 다음 섹션으로 넘어갈 수 있는 경우 JumpToSection() 함수를 통해 다음 섹션으로 넘어가고 다시 타이머를 설정합니다.
만약 추가적인 콤보 입력이 들어오지 않은 경우(bIsComboInput == false) 애니메이션 몽타주는 종료되며 ComboEnd() 함수를 호출하게 됩니다.
콤보 종료 시 콤보와 관련된 모든 변수들을 초기화하여 다음 콤보가 시작될 수 있도록 만들어줍니다.
콤보 공격 전체 코드
CombatComponent.cpp
void UTA_CombatComponent::Attack()
{
// 공격중이 아닌 경우
if (!bIsAttacking)
{
// 공중에 떠있는 경우
if (OwnerPlayer->GetCharacterMovement()->IsFalling())
{
JumpAttack();
}
// 공중에 떠있지 않은 경우
else
{
ComboStart();
bIsAttacking = true;
}
}
// 공격 중인 경우 (콤보 타이머가 정상적으로 작동하는 경우)
else if (ComboTimerHandle.IsValid())
{
// 콤보 입력이 들어왔다고 설정
bIsComboInput = true;
}
else
{
bIsComboInput = false;
}
}
void UTA_CombatComponent::AttackMove(float InAttackMoveForce)
{
if (!OwnerPlayer) return;
// 이동할 방향 + 힘 지정
FVector Impulse = OwnerPlayer->GetActorForwardVector() * InAttackMoveForce;
OwnerPlayer->GetCharacterMovement()->AddImpulse(Impulse, true);
}
void UTA_CombatComponent::ComboStart()
{
if (!ComboAttackDatas.Find(EquippedState) || !IsValid(ComboAttackDatas[EquippedState])) return;
// 임시 상태 저장
if (CombatState == ECombatState::CS_Idle || CombatState == ECombatState::CS_Dash)
{
TempState = CombatState;
}
ChangeState(ECombatState::CS_Attack);
// 현재 콤보 수 1로 설정
ComboCount = 1;
// 애님 인스턴스 받아오기
UAnimInstance* AnimInstance = OwnerPlayer->GetMesh()->GetAnimInstance();
if (!AnimInstance) return;
// 콤보 몽타주 재생
AnimInstance->Montage_Play(ComboAttackDatas[EquippedState]->ComboMontage);
// 몽타주 종료 이벤트 바인딩
FOnMontageEnded MontageEndDelegate;
MontageEndDelegate.BindUObject(this, &UTA_CombatComponent::EndCombo);
AnimInstance->Montage_SetEndDelegate(MontageEndDelegate, ComboAttackDatas[EquippedState]->ComboMontage);
// 타이머 핸들 초기화
ComboTimerHandle.Invalidate();
// 타이머 설정
SetComboTimer();
}
void UTA_CombatComponent::EndCombo(UAnimMontage* Montage, bool IsEnded)
{
// 콤보 관련 변수 초기화
bIsAttacking = false;
bIsComboInput = false;
ComboCount = 0;
if (CombatState == ECombatState::CS_Attack)
ChangeState(TempState);
}
void UTA_CombatComponent::SetComboTimer()
{
// 콤보 인덱스 저장
// * 콤보 카운트 : 1, 2, 3 ...
// * 콤보 인덱스 : 0, 1, 2 ...
int32 ComboIndex = ComboCount - 1;
// 콤보 인덱스의 콤보 체크 타이머 설정
// * 체크 인덱스가 유효한 경우
if (IsValid(ComboAttackDatas[EquippedState]) && ComboAttackDatas[EquippedState]->ComboFrame.IsValidIndex(ComboIndex))
{
// 콤보 체크 타이머 설정
GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &UTA_CombatComponent::CheckCombo, ComboAttackDatas[EquippedState]->ComboFrame[ComboIndex], false);
}
}
void UTA_CombatComponent::CheckCombo()
{
// 타이머 초기화
ComboTimerHandle.Invalidate();
// 콤보 입력이 들어온 경우
if (bIsComboInput)
{
// 콤보 수 증가
ComboCount++;
// 콤보 수가 최대 콤보 수를 넘지 않은 경우
if (ComboAttackDatas[EquippedState]->MaxCount >= ComboCount)
{
// 애님 인스턴스 받아오기
UAnimInstance* AnimInstance = OwnerPlayer->GetMesh()->GetAnimInstance();
if (!AnimInstance) return;
// 콤보 섹션 이름 저장
FName ComboSectionName = *FString::Printf(TEXT("%s%d"), *ComboAttackDatas[EquippedState]->SectionBaseName, ComboCount);
// 몽타주 섹션 이동
AnimInstance->Montage_JumpToSection(ComboSectionName, ComboAttackDatas[EquippedState]->ComboMontage);
// 타이머 설정
SetComboTimer();
// 콤보 입력 초기화
bIsComboInput = false;
}
}
}
결과
'프로젝트 > TimelessAdventure' 카테고리의 다른 글
[UE Team Project/T.A.] 7. 아이템 및 인벤토리 - 1 (Component) (0) | 2024.10.10 |
---|---|
[UE Team Project/T.A.] 6. 원거리 공격(활) 구현 / 에임 오프셋 (0) | 2024.09.30 |
[UE Team Project/T.A.] 4. 플레이어 달리기 / 구르기 구현 (0) | 2024.09.29 |
[UE Team Project/T.A.] 3. 플레이어 입력 및 기본 이동 (EnhancedInput) (0) | 2024.09.20 |
[UE Team Project/T.A.] 2. 레벨 디자인 (Landscape / Paint / Foliage) (0) | 2024.09.16 |