PART 7 - 강의 2/8

Tick 최적화

CPU 성능의 핵심, 불필요한 Tick을 제거하고 효율적으로 관리하기

01

Tick이란?

매 프레임 실행되는 함수의 비용

Tick 함수는 매 프레임 실행됩니다. 60 FPS 게임에서는 초당 60번, 수백 개의 액터가 있다면 그만큼 Tick 호출이 발생합니다.

성능 영향

대규모 오픈월드에서 수천 개의 액터가 Tick을 실행하면 CPU 성능에 심각한 영향을 미칩니다. dumpticks 명령어로 현재 틱킹 중인 객체를 확인하세요.

Console Commands
// 틱킹 객체 목록 확인 dumpticks // 모든 틱킹 액터/컴포넌트 dumpticks grouped // 그룹별로 정렬 listtimers // 활성화된 타이머 목록
02

Tick 비활성화 전략

필요할 때만 Tick을 활성화

Level 1: 완전 비활성화

L1
Tick이 필요 없는 경우

정적 오브젝트, 이벤트 기반 로직만 있는 액터

C++
// 생성자에서 Tick 완전 비활성화 AMyStaticActor::AMyStaticActor() { // Tick 기능 자체를 사용하지 않음 PrimaryActorTick.bCanEverTick = false; } // 컴포넌트도 마찬가지 UMyComponent::UMyComponent() { PrimaryComponentTick.bCanEverTick = false; }

Level 2: 조건부 활성화

L2
특정 상황에서만 Tick 필요

애니메이션 재생 중, 이동 중, 상호작용 중일 때만

C++
AMyActor::AMyActor() { // Tick 가능하지만, 시작 시 비활성화 PrimaryActorTick.bCanEverTick = true; PrimaryActorTick.bStartWithTickEnabled = false; } // 필요할 때 활성화 void AMyActor::StartMoving() { SetActorTickEnabled(true); bIsMoving = true; } // 완료 후 비활성화 void AMyActor::StopMoving() { SetActorTickEnabled(false); bIsMoving = false; } void AMyActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 이동 로직 수행 UpdateMovement(DeltaTime); // 목적지 도착 시 Tick 비활성화 if (HasReachedDestination()) { StopMoving(); } }

Level 3: Tick Interval 조절

L3
매 프레임이 아닌 간격으로 실행

AI 의사결정, 거리 체크 등 빈번하지 않아도 되는 로직

C++
AMyAIActor::AMyAIActor() { PrimaryActorTick.bCanEverTick = true; // 0.1초마다 Tick (10 FPS) PrimaryActorTick.TickInterval = 0.1f; } // 또는 런타임에 동적으로 변경 void AMyAIActor::SetTickFrequency(float Interval) { PrimaryActorTick.TickInterval = Interval; } // 거리에 따라 Tick Interval 조절 void AMyAIActor::AdjustTickByDistance(float Distance) { if (Distance < 1000.f) { // 가까우면 자주 업데이트 PrimaryActorTick.TickInterval = 0.033f; // 30 FPS } else if (Distance < 3000.f) { PrimaryActorTick.TickInterval = 0.1f; // 10 FPS } else { PrimaryActorTick.TickInterval = 0.5f; // 2 FPS } }
03

컴포넌트 Tick 최적화

액터뿐 아니라 컴포넌트도 최적화

C++
UMyAnimationComponent::UMyAnimationComponent() { PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.bStartWithTickEnabled = false; } void UMyAnimationComponent::PlayAnimation() { // 애니메이션 시작 시 Tick 활성화 SetComponentTickEnabled(true); ResetAnimationState(); } void UMyAnimationComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); // 애니메이션 업데이트 UpdateAnimation(DeltaTime); // 완료되면 Tick 비활성화 if (IsAnimationComplete()) { SetComponentTickEnabled(false); OnAnimationFinished.Broadcast(); } }
04

Timer 활용

Tick 대신 Timer 사용하기

비효율적 매 프레임 체크
void Tick(float DeltaTime) { Timer += DeltaTime; if (Timer >= 1.0f) { DoSomething(); Timer = 0.f; } }
효율적 Timer 사용
void BeginPlay() { GetWorldTimerManager().SetTimer( TimerHandle, this, &AMyActor::DoSomething, 1.0f, // 1초마다 true // 반복 ); }

Timer 활용 예제

C++
UCLASS() class AMyActor : public AActor { GENERATED_BODY() public: AMyActor() { // Tick 완전 비활성화 PrimaryActorTick.bCanEverTick = false; } virtual void BeginPlay() override { Super::BeginPlay(); // 1초마다 반복 실행 GetWorldTimerManager().SetTimer( HealthRegenHandle, this, &AMyActor::RegenerateHealth, 1.0f, true ); // 5초 후 한 번만 실행 GetWorldTimerManager().SetTimer( SpawnHandle, this, &AMyActor::SpawnReinforcements, 5.0f, false ); } virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override { // Timer 정리 GetWorldTimerManager().ClearTimer(HealthRegenHandle); GetWorldTimerManager().ClearTimer(SpawnHandle); Super::EndPlay(EndPlayReason); } private: FTimerHandle HealthRegenHandle; FTimerHandle SpawnHandle; void RegenerateHealth(); void SpawnReinforcements(); };
Timer vs Tick

Timer는 내부적으로 최적화되어 있어 Tick보다 효율적입니다. 특히 1초 이상의 간격이나 일회성 작업에 적합합니다. listtimers 명령으로 활성화된 타이머를 확인할 수 있습니다.

05

이벤트 기반 설계

폴링 대신 이벤트 사용

폴링 방식
void Tick(float DeltaTime) { // 매 프레임 체력 확인 if (Health <= 0) { Die(); } }
이벤트 방식
void TakeDamage(float Damage) { Health -= Damage; // 변경 시점에만 체크 if (Health <= 0) { Die(); } }
C++ - 델리게이트 활용
// 이벤트 정의 DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealth); UCLASS() class UHealthComponent : public UActorComponent { GENERATED_BODY() public: UHealthComponent() { // Tick 불필요 PrimaryComponentTick.bCanEverTick = false; } UPROPERTY(BlueprintAssignable) FOnHealthChanged OnHealthChanged; void TakeDamage(float Damage) { float OldHealth = Health; Health = FMath::Clamp(Health - Damage, 0.f, MaxHealth); if (Health != OldHealth) { // 변경 시에만 이벤트 발생 OnHealthChanged.Broadcast(Health); } } private: float Health = 100.f; float MaxHealth = 100.f; };
SUMMARY

핵심 요약

핵심 포인트
  • bCanEverTick = false - Tick이 필요 없는 액터는 완전히 비활성화
  • bStartWithTickEnabled = false - 필요할 때만 Tick 활성화
  • TickInterval - 매 프레임 대신 간격을 두고 실행
  • Timer 사용 - 주기적 작업은 Tick 대신 Timer 활용
  • 이벤트 기반 설계 - 폴링 대신 상태 변경 시 이벤트 발생
  • dumpticks - 현재 틱킹 객체 목록 확인
최적화 우선순위

1. Tick 완전 비활성화 > 2. 조건부 활성화 > 3. Tick Interval 조절 > 4. Timer 대체

PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: 불필요한 Tick 비활성화

PrimaryActorTick.bCanEverTick = false로 틱이 필요 없는 RPG Actor(정적 소품, 환경 오브젝트)의 틱을 비활성화하세요. stat DumpTicks로 틱 중인 Actor 수를 확인하세요.

실습 2: 타이머와 이벤트로 Tick 대체

GetWorldTimerManager().SetTimer()로 주기적 체크(몬스터 어그로 범위, 버프 지속시간)를 대체하세요. 델리게이트 기반 이벤트로 상태 변경 시에만 로직을 실행하도록 리팩터링하세요.

심화 과제: SignificanceManager 기반 틱 제어

USignificanceManager를 활용하여 카메라 거리에 따라 NPC의 틱 빈도를 동적으로 조절하세요. 가까운 NPC는 매 프레임, 먼 NPC는 0.5초마다 틱하도록 설정하세요.