프로젝트 회고 / / 2025. 12. 2. 23:00

[UE5 액션] 유연한 콤보 시스템: 이벤트 기반 Perfect/Mercy 구간을 통한 공격 후딜레이 캔슬 및 콤보 연계

반응형

🎮 구현 목표

자연스러운 콤보 시스템 구현

  • 공격 중 입력이 들어오면 후딜레이를 캔슬하고 즉시 다음 콤보로 연계
  • 공격 종료 후에도 일정 시간 내 입력 시 다음 콤보 진행
  • 입력 타이밍에 따라 정확한 프레임에서 콤보 전환

 

🚨 기존 방식

 

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는 시간 기반이므로 애니메이션의 특정 구간을 정밀하게 제어하기 어렵습니다. 필요한 것은:

  1. 입력 받을 수 있는 구간 정의 (공격 시작 ~ 후딜레이 전)
  2. 해당 구간 내에서만 입력 체크
  3. 입력이 들어온 순간 바로 다음 콤보 전환 가능

 

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차 개선: 이중 입력 체크

구현 목표:

  1. Perfect Timing 보상: ComboWindow 내 입력 → 후딜 캔슬, 부드러운 연계
  2. 패널티 부여: ComboWindow 놓침 → 후딜레이 재생 (시간적 불이익)
  3. 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. 콤보 공격 재생(1타)
  2. ComboWindow 놓침 → 후딜 재생 (패널티)
  3. 후딜 중 다시 입력
  4. 후딜레이 중단 → 즉시 2타 공격 진행! 

 

 

✅ 결과

조작감을 챙긴 콤보 시스템

Perfect Timing 보상 (ComboWindow)

  • 공격 모션 30% 이후 입력 → 후딜 캔슬
  • 숙련된 플레이어에게 빠른 전투감 제공

Mercy Window (Extra Combo Input)

  • ComboWindow 놓쳐도 0.N초 추가 기회
  • 후딜레이 패널티는 있지만 콤보는 끊기지 않음

입력 타이밍별 차등 보상
| 타이밍 | 조작감 | 전투 속도 | 콤보 유지 |
|--------|--------|----------|----------|
| Perfect | 후딜 없음 | 빠름 ⭐ | ✅ |
| Late | 후딜 재생 | 보통 ✅ | ✅ |
| Too Late | 콤보 끊김 | 느림 ❌ | ❌ |

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유