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 |