반응형
🎮 목표
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로 보장하는 것이 안전합니다. 언리얼의 이벤트 시스템을 적극 활용하면 코드가 더 명확하고 유지보수가 쉬워집니다.
반응형
'프로젝트 회고' 카테고리의 다른 글
| [Dedicated Server] 네트워크 복제 대역폭 최적화: ```Fast Array Serializer```를 활용한 배열 요소 단위 델타 복제(Delta Replication) (0) | 2025.12.03 |
|---|---|
| PlayerController가 입력을 처리하는게 적절한가? (0) | 2025.12.03 |
| [DirectX 11] Unity 리소스 메타데이터 파싱을 통한 스프라이트 시트 관리 (0) | 2025.12.02 |
| [UE5 팀 프로젝트] Pull-Request 시행착오와 교훈 (0) | 2025.12.02 |
| [Dedicated Server] 콜백(Callback) 체인을 활용한 데이터 무결성 확보 (0) | 2025.12.02 |
