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

[UE5 액션] [리팩토링] CPU 성능 최적화: Tick 의존성 해소, 이벤트 기반(Event-Driven Architecture)으로의 전환

반응형

🎮 목표

Tick 사용을 최소화하고 이벤트 기반으로 게임 로직 구성하기

 

💭 고민

 

"Tick에 익숙해진 습관"

 

기존 프로젝트에서 Tick 의존도:

  • WinAPI: 거의 모든 로직
  • DirectX: FSM의 ComponentTick
  • UE5 초기: 여전히 Tick 남용

이면에는 Tick은 직관적이고 "이벤트 기반은 낯설다"는 생각이 있었습니다.

 

성능 문제 경험:

// WinAPI 시절 실수
void Player::Tick()
{
    UpdateUI();  // 매 프레임 UI 리소스 갱신
    // 결과: 800fps → 150fps ❌
}

 

이번 프로젝트는 철저히 Timer, Delegate, AnimNotify 활용을 목표로 했습니다.

AnimNotify의 함정

 

초기엔 AnimNotify가 너무 편해서 남용:

// 공격 종료 시 상태 초기화
AnimNotify_RemoveGameplayTag → RemoveState(Attacking)

 

문제 발생:

  • 공격 중 회피 → 몽타주 중단 → AnimNotify_RemoveGameplayTag 미호출 ❌
  • 블렌드 아웃 중 끝부분 Notify 누락
  • Character_State_Attacking 영구 유지 → 캐릭터 이동 불가

깨달음: "AnimNotify는 호출을 보장하지 않는다"

 

🔧 해결 방법

1. 중요 로직은 Delegate로 보장

// Before: AnimNotify (불안정)
AnimNotify_AttackEnd → 상태 초기화

// After: FOnMontageEnded (안전)
void ASoulCharacterBase::DoAttack(...)
{
    FOnMontageEnded OnMontageEnded;
    OnMontageEnded.BindUObject(this, &ThisClass::RecoveryAttack);

    AnimInstance->Montage_Play(AttackMontage);
    AnimInstance->Montage_SetEndDelegate(OnMontageEnded, AttackMontage);
}

void ASoulCharacterBase::RecoveryAttack(UAnimMontage* Montage, bool bInterrupted)
{
    // 중단되든 정상 종료든 무조건 호출 ✅
    RemoveState(SoulGameplayTag::Character_State_Attacking);
}

2. UI는 Delegate로 분리

// Before: UI가 Component 직접 참조
void UHealthBarWidget::NativeTick()
{
    AttributeComp = GetOwner()->GetComponent();
    UpdateHealth(AttributeComp->GetHealth());  // 매 프레임 ❌
}
// After: Delegate 구독
void UAttributeComponent::TakeDamageAmount(float Damage)
{
    BaseHealth -= Damage;
    OnAttributeChanged.Broadcast(EAttributeType::Health, BaseHealth, MaxHealth);
}

void UStatBarWidget::BindDelegate()
{
    AttributeComponent->OnAttributeChanged.AddDynamic(
        this, &UStatBarWidget::SetPercent);
}

 

3. 역할별 이벤트 활용

상황 Tick (Before) 이벤트 (After)
스태미나 회복 매 프레임 체크 Timer (회복 주기)
상태 해제 bool 변수 체크 Delegate
UI 갱신 매 프레임 조회 Delegate (변경 시만)
충돌 검사 매 프레임 AnimNotifyState (구간 지정)
무적 부여 bool + Tick AnimNotifyState (Begin/End)

 

 

최종 Tick 사용처:

// WeaponCollisionComponent.cpp - 충돌 검사만
void UWeaponCollisionComponent::TickComponent(float DeltaTime, ...)
{
    if (bIsCollisionEnabled)  // 공격 중에만 활성화
    {
        CollisionTrace();  // 무기 궤적 추적
    }
}

// TargetingComponent.cpp - 락온 유지
void UTargetingComponent::TickComponent(float DeltaTime, ...)
{
    if (bIsLockOn && IsValid(LockedTargetActor))
    {
        FaceLockOnActor();  // 시선 고정
    }
}

 

 

온/오프 제어로 최적화:

void ActivateCollision()
{
    WeaponCollisionComponent->SetComponentTickEnabled(true);
}

void DeactivateCollision()
{
    WeaponCollisionComponent->SetComponentTickEnabled(false);
}

 

✅ 결과

 

Tick 사용 최소화: AI 외 거의 미사용
명확한 실행 타이밍: "언제" 호출되는지 코드로 명시
디버깅 용이: Delegate/Timer는 호출 시점 추적 쉬움
성능 향상: 불필요한 매 프레임 체크 제거

 

 

Tick의 함정:

  • 매 프레임 실행 = 성능 낭비
  • "언제" 실행되는지 불명확
  • bool 변수 남발

 

이벤트 기반의 장점:

  • Timer: "3초 후 실행" 명확
  • Delegate: "HP 변경 시 UI 갱신" 명확
  • AnimNotify: "공격 30프레임에 충돌 활성화" 명확

 

AnimNotify는 "타이밍 이벤트"로만 사용하고, "상태 관리"는 Delegate로 보장하는 것이 안전합니다. 언리얼의 이벤트 시스템을 적극 활용하면 코드가 더 명확하고 유지보수가 쉬워집니다.

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