🎮 구현 목표
자연스러운 콤보 시스템 구현
- 공격 중 입력이 들어오면 후딜레이를 캔슬하고 즉시 다음 콤보로 연계
- 공격 종료 후에도 일정 시간 내 입력 시 다음 콤보 진행
- 입력 타이밍에 따라 정확한 프레임에서 콤보 전환
🚨 기존 방식
Timer 기반 콤보의 한계: 뚝뚝 끊기는 연계
기존 시스템은 공격 몽타주 재생 시간을 기준으로 Timer를 설정하고, Timer 종료 시점에 입력 여부를 확인하여 다음 콤보를 재생했습니다.
void ACharacter::Attack()
{
// 몽타주 전체 길이로 타이머 설정
float MontageLength = AttackMontage->GetPlayLength();
GetWorldTimerManager().SetTimer(ComboCheckTimer, this,
&ThisClass::CheckComboInput, MontageLength, false);
PlayAnimMontage(AttackMontage);
}
void ACharacter::CheckComboInput()
{
if (bInputPressed)
{
PlayNextCombo(); // 타이머 종료 시점에만 체크
}
}
발생한 문제:
- ❌ 콤보 전환 타이밍이 몽타주 끝으로 고정되어 부자연스러움
- ❌ 공격 모션이 끝나기 전 입력해도 타이머가 끝날 때까지 대기
- ❌ 후딜레이 캔슬이 불가능해 액션감 저하
핵심 문제: "입력을 언제 받았는가"와 "언제 다음 콤보로 넘어갈 수 있는가"를 분리할 수 없었습니다.
💭 해결 방안
"입력 체크 타이밍을 구간으로 나눠 관리하고 싶다."
Timer는 시간 기반이므로 애니메이션의 특정 구간을 정밀하게 제어하기 어렵습니다. 필요한 것은:
- 입력 받을 수 있는 구간 정의 (공격 시작 ~ 후딜레이 전)
- 해당 구간 내에서만 입력 체크
- 입력이 들어온 순간 바로 다음 콤보 전환 가능
AnimNotifyState의 발견:
AnimNotify가 단일 시점에서만 호출되는 반면, AnimNotifyState는:
NotifyBegin: 구간 시작 시점NotifyTick: 구간 내 매 프레임NotifyEnd: 구간 종료 시점
이 세 가지 콜백을 제공하여 시간 구간을 정밀하게 제어할 수 있습니다.
🔧 시행착오
1차 시도: AnimNotifyState로 입력 윈도우 구현
핵심 아이디어: 공격 모션이 절반 이상 진행된 시점부터 입력을 받을 수 있는 "콤보 윈도우"를 정의
void UAnimNotifyState_ComboWindow::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
float TotalDuration, const FAnimNotifyEventReference& EventReference)
{
if (UCombatComponent* CombatComp = Character->GetCombatComponent())
{
CombatComp->EnableComboWindow();
}
}
void UAnimNotifyState_ComboWindow::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference)
{
if (UCombatComponent* CombatComp = Character->GetCombatComponent())
{
CombatComp->DisableComboWindow();
}
}
좋은 점:
- ✅ ComboWindow 내 입력 → 후딜레이 캔슬하고 즉시 다음 콤보
- ✅ 정확한 타이밍에 입력한 플레이어에게 빠른 전투감 제공
void UCombatComponent::DisableComboWindow()
{
bCanComboInput = false;
// 애니메이션 재생 중에 다음 공격 입력이 들어왔다면 다음 애니메이션 재생 준비
if (bSavedComboInput)
{
bSavedComboInput = false;
++ComboCounter;
LOG_WARNING("Combo Window Closed : 입력 확인, 다음 콤보 실행");
// 다음 콤보 재생
DoAttack(LastAttackType);
}
}
딜레마
ComboWindow를 벗어나면 입력이 모두 무시되어 콤보가 첫 번째로 초기화됩니다.
플레이어 경험:
1. 1타 공격 → 2타 입력 (살짝 늦음)
2. ComboWindow 놓침 → 후딜레이 진입
3. 후딜 중 다시 입력
4. 콤보 초기화 → 1타부터 다시 시작 😡
"타이밍 놓쳤으니 다시 처음부터" = 매우 불쾌한 경험
조작감을 높이겠다는 의도로 후딜레이 캔슬이라는 혜택을 제공한 것은 좋았으나, 패널티(후딜레이 간 입력 불가/ 이동 불가)가 너무 강력했습니다.
2차 시도: ComboWindow 범위 확장
시도: NotifyState의 범위를 후딜레이 구간까지 늘려보자
✅ 장점:
- 입력 허용 시간이 넉넉해져 콤보 성공률 ↑
❌ 치명적 단점:
플레이어 경험:
1. 1타 완료 → 후딜레이 진입
2. 후딜 중 2타 입력 (입력은 받아짐)
3. 그러나 NotifyEnd() 도달 전까지 대기 😤
4. 콤보 공격 진행이 느릿하게 느껴짐
후딜레이를 경험하는 건 매한가지입니다. Timer만 사용했을 때에 비해 두드러지는 개선감이 잘 느껴지지 않습니다.
핵심 문제 인식:
앞쪽으로 여유 주기 (공격 중) ✅ → 후딜 캔슬 = 쾌적함
뒤쪽으로 여유 주기 (후딜레이 중) ❌ → 후딜 대기 = 불쾌감
AnimNotifyState는 구간의 끝에서 일괄 처리하는 구조이므로, 후딜레이 구간까지 포함하면 결국 후딜을 다 봐야 합니다.
3차 개선: 이중 입력 체크
구현 목표:
- Perfect Timing 보상: ComboWindow 내 입력 → 후딜 캔슬, 부드러운 연계
- 패널티 부여: ComboWindow 놓침 → 후딜레이 재생 (시간적 불이익)
- 2차 기회 제공: 그래도 후딜 중 입력 들어오면 → 콤보는 이어짐
핵심 아이디어:
- 1차 윈도우 (ComboWindow): 후딜 캔슬 가능한 "Perfect 구간"
- 2차 윈도우 (Timer): 후딜은 보지만 콤보는 이어지는 "Mercy 구간"
void UAnimNotify_AttackFinished::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference)
{
if (UCombatComponent* CombatComp = Character->GetCombatComponent())
{
CombatComp->AttackFinished(ComboResetDelay);
}
}
void UCombatComponent::AttackFinished(const float ComboResetDelay)
{
// ComboResetDelay 간 추가 콤보 입력 시간을 준 뒤 콤보 시퀀스 종료
// 입력이 들어오면 ExecuteComboAttack에서 콤보 공격을 자동으로 처리. 타이머는 콤보 초기화만 지연
GetWorld()->GetTimerManager().SetTimer(ComboResetTimerHandle, this,
&UCombatComponent::ResetCombo, ComboResetDelay);
}
void UCombatComponent::ExecuteComboAttack(const FGameplayTag& AttackTypeTag)
{
if (false == CharacterBase->IsCurrentState(SoulGameplayTag::Character_State_Attacking))
{
// 애니메이션은 끝났지만, 콤보시퀀스 변수가 true인 시간에는 추가 입력 기회를 준다.
if (bComboSequenceRunning == true && bCanComboInput == false)
{
++ComboCounter;
LOG_WARNING("추가 공격 입력 기회 : Combo counter : %d", ComboCounter);
}
else // 첫 번째 공격
{
LOG(">>> 콤보 시퀀스 시작 <<<");
ResetCombo();
bComboSequenceRunning = true;
}
// 진짜 공격 로직 처리 함수
DoAttack(AttackTypeTag);
GetWorld()->GetTimerManager().ClearTimer(ComboResetTimerHandle);
}
// 아직 공격 애니메이션이 끝나지 않았는데 콤보 입력이 추가로 들어온 경우 : 최적의 타이밍
else if (bCanComboInput)
{
LOG_WARNING("Combo Hit!!!!");
bSavedComboInput = true;
}
}
플레이어 경험 개선:
| 시나리오 | 기존 (Timer만) | 개선 (이중 윈도우) |
|---|---|---|
| Perfect 입력 | 후딜 재생 → 다음 콤보 😡 | 공격 끝 → 즉시 다음 콤보 ✅ |
| ComboWindow 놓침 | - | 후딜 재생 😡 |
| 후딜 중 입력 | 후딜 재생 → 다음 콤보 😡 | 즉시 다음 콤보 ✅ |
| Timer 종료 후 | 1타로 되돌아감 | 1타로 되돌아감 |
| ``` |
개선된 플레이어 경험:
- 콤보 공격 재생(1타)
- ComboWindow 놓침 → 후딜 재생 (패널티)
- 후딜 중 다시 입력
- 후딜레이 중단 → 즉시 2타 공격 진행!
✅ 결과
조작감을 챙긴 콤보 시스템
✅ Perfect Timing 보상 (ComboWindow)
- 공격 모션 30% 이후 입력 → 후딜 캔슬
- 숙련된 플레이어에게 빠른 전투감 제공
✅ Mercy Window (Extra Combo Input)
- ComboWindow 놓쳐도 0.N초 추가 기회
- 후딜레이 패널티는 있지만 콤보는 끊기지 않음
✅ 입력 타이밍별 차등 보상
| 타이밍 | 조작감 | 전투 속도 | 콤보 유지 |
|--------|--------|----------|----------|
| Perfect | 후딜 없음 | 빠름 ⭐ | ✅ |
| Late | 후딜 재생 | 보통 ✅ | ✅ |
| Too Late | 콤보 끊김 | 느림 ❌ | ❌ |
'프로젝트 회고' 카테고리의 다른 글
| [UE5 액션] 데이터 에셋 기반 무기별 전투 스타일 관리 (0) | 2025.12.02 |
|---|---|
| [UE5 액션] 루트 모션 기반 자연스러운 대시 구현 Motion Warping (0) | 2025.12.02 |
| [DirectX 11] 다중 좌표계 간 위치 동기화 (0) | 2025.12.02 |
| [UE5 액션] 애니메이션 라이프 사이클 기반 비동기 상태 관리 (0) | 2025.12.02 |
| [UE5 액션] 상태 관리: StateComponent와 GameplayTag (0) | 2025.12.02 |
