Unreal의 Tick()은 매 프레임마다 실행되는 함수다.
60FPS라면 초당 60번, 30FPS라면 초당 30번 호출된다.


Tick의 기본 개념

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    UE_LOG(LogTemp, Warning, TEXT("Tick 실행! DeltaTime: %f"), DeltaTime);
}

DeltaTime은 이전 프레임과 현재 프레임 사이의 시간 차이다.

FPS DeltaTime
60FPS 약 0.016초
30FPS 약 0.033초

DeltaTime을 사용하지 않으면 프레임 수에 따라 이동 속도가 달라질 수 있다.

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // 프레임 의존 이동
    SetActorLocation(GetActorLocation() + FVector(5, 0, 0));
}

60FPS에서는 초당 300 유닛 이동하지만, 30FPS에서는 초당 150 유닛만 이동한다.

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // 프레임 독립 이동
    float Speed = 100.0f;
    FVector Movement = FVector(Speed * DeltaTime, 0, 0);

    SetActorLocation(GetActorLocation() + Movement);
}
방식 결과
프레임마다 고정값 이동 FPS에 따라 속도 달라짐
DeltaTime 기반 이동 FPS와 무관하게 일정한 속도 유지

Tick의 누적 비용

Actor 하나의 Tick 비용이 작더라도, Actor 수가 많아지면 비용이 누적된다.

Actor 수 Tick 비용 예시
1개 0.05ms
100개 5ms
1,000개 50ms
10,000개 500ms

60FPS의 한 프레임 예산은 약 16.67ms다.

작업 예시 비용
입력 처리 1ms
게임 로직 Tick 10ms
물리 3ms
AI 2ms
애니메이션 2ms
렌더링 5ms
총합 23ms

23ms는 60FPS 기준 프레임 예산인 16.67ms를 초과한다.
이 경우 프레임 드랍이 발생할 수 있다.


무거운 Tick 예시

void AMonster::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    ACharacter* Player = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);

    if (Player)
    {
        float Distance = FVector::Dist(GetActorLocation(), Player->GetActorLocation());

        if (Distance > AttackRange)
        {
            TArray<FVector> Path = FindPathToPlayer();
            MoveAlongPath(Path);
        }
        else
        {
            Attack(Player);
        }
    }
}
문제 설명
매 프레임 플레이어 검색 몬스터마다 GetPlayerCharacter() 호출
매 프레임 거리 계산 FVector::Dist()는 내부적으로 sqrt 연산 포함
매 프레임 경로 탐색 A* 같은 경로 탐색은 비용이 큼
Actor별 개별 Tick 같은 작업이 수백 번 반복됨

몬스터 200마리가 60FPS에서 위 로직을 실행하면, 플레이어 검색과 거리 계산이 초당 12,000번 발생한다.

항목 계산
몬스터 수 200
FPS 60
초당 반복 횟수 12,000

Tick 비용 확인

stat game

콘솔에서 stat game을 입력하면 게임 스레드 비용을 확인할 수 있다.

항목 의미
Frame 전체 프레임 시간
Game 게임 스레드 시간. Tick 포함
Draw 렌더 스레드 준비 시간
GPU GPU 처리 시간

Game 시간이 10ms 이상이면 Tick, AI, 게임 로직 쪽 병목을 의심할 수 있다.

직접 측정

void AMyActor::Tick(float DeltaTime)
{
    double StartTime = FPlatformTime::Seconds();

    DoHeavyWork();

    double ElapsedTime = FPlatformTime::Seconds() - StartTime;

    if (ElapsedTime > 0.001)
    {
        UE_LOG(LogTemp, Warning, TEXT("무거운 Tick: %.3f ms"),
            ElapsedTime * 1000);
    }
}

ElapsedTime > 0.001은 1ms 이상 걸린 Tick을 로그로 남긴다는 의미다.
특정 Actor나 특정 로직이 얼마나 비용을 쓰는지 확인할 때 유용하다.


실행 간격 조절

모든 로직을 매 프레임 실행할 필요는 없다.
AI 상태 갱신, 거리 체크, 미니맵 갱신 같은 작업은 0.1초 또는 0.5초마다 실행해도 충분한 경우가 많다.

void AEnemy::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    TimeSinceLastUpdate += DeltaTime;

    if (TimeSinceLastUpdate >= 0.1f)
    {
        TimeSinceLastUpdate = 0.0f;

        CheckPlayerDistance();
        UpdateAIState();
    }

    UpdateAnimation();
}

중복 계산 제거

void AEnemy::Tick(float DeltaTime)
{
    ACharacter* Player = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);

    FVector PlayerLoc = Player->GetActorLocation();

    float Distance = FVector::Dist(GetActorLocation(), PlayerLoc);
}

Enemy가 200마리라면 플레이어 검색, 위치 조회, 거리 계산이 각각 200번 반복된다.

void AEnemyManager::Tick(float DeltaTime)
{
    if (!CachedPlayer)
    {
        CachedPlayer = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);
    }

    FVector PlayerLocation = CachedPlayer->GetActorLocation();

    for (AEnemy* Enemy : AllEnemies)
    {
        float Distance = FVector::Dist(Enemy->GetActorLocation(), PlayerLocation);
        Enemy->SetPlayerInfo(PlayerLocation, Distance);
    }
}
기존 구조 개선 구조
Enemy마다 Player 검색 Manager가 한 번만 검색
Enemy마다 Player 위치 조회 Manager가 위치를 캐싱
Enemy마다 같은 데이터 계산 공통 데이터 공유
Actor별 Tick 증가 Manager 중심 갱신

거리별 차등 업데이트

플레이어와 가까운 Enemy는 자주 갱신하고, 먼 Enemy는 낮은 빈도로 갱신한다.

void AEnemyManager::UpdateEnemies()
{
    for (AEnemy* Enemy : AllEnemies)
    {
        float Distance = FVector::Dist(Enemy->GetActorLocation(), PlayerLocation);

        if (Distance < 500.0f)
        {
            Enemy->SetUpdateRate(0.033f);
        }
        else if (Distance < 1500.0f)
        {
            Enemy->SetUpdateRate(0.1f);
        }
        else if (Distance < 3000.0f)
        {
            Enemy->SetUpdateRate(0.5f);
        }
        else
        {
            Enemy->SetUpdateRate(1.0f);
        }
    }
}
거리 업데이트 주기 의미
근거리 0.033초 약 30FPS 수준
중거리 0.1초 초당 10번
원거리 0.5초 초당 2번
초원거리 1.0초 초당 1번

가까운 대상은 반응성이 중요하고, 먼 대상은 정확도보다 비용 절감이 중요하다.


매니저 패턴

Actor마다 Tick을 두는 대신, Manager가 여러 Actor를 묶어서 갱신한다.

class AEnemyManager : public AActor
{
private:
    float FastUpdateTimer = 0.0f;
    float NormalUpdateTimer = 0.0f;
    float SlowUpdateTimer = 0.0f;

    TArray<AEnemy*> CloseEnemies;
    TArray<AEnemy*> MediumEnemies;
    TArray<AEnemy*> FarEnemies;
};
void AEnemyManager::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    FastUpdateTimer += DeltaTime;
    NormalUpdateTimer += DeltaTime;
    SlowUpdateTimer += DeltaTime;

    if (FastUpdateTimer >= 0.05f)
    {
        FastUpdateTimer = 0.0f;
        UpdateCloseEnemies();
    }

    if (NormalUpdateTimer >= 0.1f)
    {
        NormalUpdateTimer = 0.0f;
        UpdateMediumEnemies();
    }

    if (SlowUpdateTimer >= 0.5f)
    {
        SlowUpdateTimer = 0.0f;
        UpdateFarEnemies();
        ReclassifyEnemies();
    }
}
void AEnemyManager::ReclassifyEnemies()
{
    FVector PlayerLoc = CachedPlayer->GetActorLocation();

    CloseEnemies.Empty();
    MediumEnemies.Empty();
    FarEnemies.Empty();

    for (AEnemy* Enemy : AllEnemies)
    {
        float DistSqr = FVector::DistSquared(Enemy->GetActorLocation(), PlayerLoc);

        if (DistSqr < 250000.0f)
        {
            CloseEnemies.Add(Enemy);
        }
        else if (DistSqr < 2250000.0f)
        {
            MediumEnemies.Add(Enemy);
        }
        else
        {
            FarEnemies.Add(Enemy);
        }
    }
}

FVector::DistSquared()는 sqrt 연산을 하지 않는다.
단순 거리 비교에서는 FVector::Dist()보다 적합하다.

비교 거리 제곱 거리
500 250000
1500 2250000

Tick Group

Tick Group은 프레임 안에서 Tick 실행 순서를 제어한다.

Tick Group 용도
TG_PrePhysics 물리 계산 전. 입력, 이동 명령
TG_PostPhysics 물리 계산 후. AI, 충돌 처리
TG_PostUpdateWork 프레임 후반. UI, 카메라
AMyPlayerController::AMyPlayerController()
{
    PrimaryActorTick.bCanEverTick = true;
    PrimaryActorTick.TickGroup = TG_PrePhysics;
}
AEnemy::AEnemy()
{
    PrimaryActorTick.bCanEverTick = true;
    PrimaryActorTick.TickGroup = TG_PostPhysics;
}
AFollowCamera::AFollowCamera()
{
    PrimaryActorTick.bCanEverTick = true;
    PrimaryActorTick.TickGroup = TG_PostUpdateWork;
}
작업 추천 Tick Group
플레이어 입력 TG_PrePhysics
이동 명령 TG_PrePhysics
물리 결과 기반 판정 TG_PostPhysics
AI 상태 갱신 TG_PostPhysics
카메라 후처리 TG_PostUpdateWork
UI 갱신 TG_PostUpdateWork

최적화 효과

최적화 방법 기대 효과
실행 간격 조절 불필요한 매 프레임 실행 제거
중복 계산 제거 같은 연산을 Manager에서 한 번만 처리
거리별 차등 업데이트 중요도에 따라 갱신 빈도 조절
DistSquared() 사용 sqrt 비용 제거
매니저 패턴 다수 Actor Tick을 한 곳에서 관리
Tick Group 설정 프레임 내 실행 순서 명확화
최적화 방법 예시 개선율
실행 간격 조절 약 6배
중복 계산 제거 약 200배
연산 최적화 약 30%
거리별 차등 약 3배
매니저 패턴 약 10배

원칙 설명
매 프레임 필요한지 확인 AI, 거리 체크, 미니맵은 보통 매 프레임 필요 없음
실행 주기 낮추기 0.1초, 0.5초 단위 갱신으로 비용 절감
중복 계산 제거 Manager에서 공통 데이터 캐싱
거리별 차등 갱신 가까운 대상만 자주 업데이트
무거운 연산 제거 Tick 안에서 경로 탐색, 전체 검색, Spawn 남발 금지
Tick Group 활용 입력, 물리, 카메라의 실행 순서 정리
반응형

'Unreal Engine' 카테고리의 다른 글

Unreal Engine GC  (0) 2026.07.08
Unreal Engine Singleton  (0) 2026.07.07
UE 애니메이션 리타게팅  (0) 2026.07.03
언리얼 모듈, Build.cs, 리플렉션  (0) 2026.07.02
UE MVVM Plugin  (0) 2026.07.01