액터 1개의 Tick 함수가 0.05ms가 걸린다고 한다면, 액터가 1000개 있으면 50ms나 걸린다.
void AMyActor::Tick(float DeltaTime)
{
double StartTime = FPlatformTime::Seconds();
DoHeavyWork();
double ElapsedTime = FPlatformTime::Seconds() - StartTime;
if (ElapsedTime > 0.001) // 1ms 이상
{
UE_LOG(LogTemp, Warning, TEXT("무거운 Tick: %.3f ms"),
ElapsedTime * 1000);
}
}
void AEnemy::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
TimeSinceLastUpdate += DeltaTime;
if (TimeSinceLastUpdate >= 0.1f) // 0.1초마다
{
TimeSinceLastUpdate = 0.0f;
CheckPlayerDistance();
UpdateAIState();
}
UpdateAnimation(); // 애니메이션은 매 프레임
}
void AEnemyManager::Tick(float DeltaTime)
{
if (!CachedPlayer)
CachedPlayer = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);
FVector PlayerLocation = CachedPlayer->GetActorLocation(); // 1번만
for (AEnemy* Enemy : AllEnemies)
{
float Distance = FVector::Dist(Enemy->GetActorLocation(), PlayerLocation);
Enemy->SetPlayerInfo(PlayerLocation, Distance);
}
}
void AEnemyManager::UpdateEnemies()
{
for (AEnemy* Enemy : AllEnemies)
{
float Distance = FVector::Dist(Enemy->GetActorLocation(), PlayerLocation);
if (Distance < 500.0f) // 근거리
Enemy->SetUpdateRate(0.033f); // 30FPS
else if (Distance < 1500.0f) // 중거리
Enemy->SetUpdateRate(0.1f); // 10FPS
else if (Distance < 3000.0f) // 원거리
Enemy->SetUpdateRate(0.5f); // 2FPS
else // 초원거리
Enemy->SetUpdateRate(1.0f); // 1FPS
}
}
class AEnemyManager : public AActor
{
private:
float FastUpdateTimer = 0.0f; // 0.05초 주기
float NormalUpdateTimer = 0.0f; // 0.1초 주기
float SlowUpdateTimer = 0.0f; // 0.5초 주기
TArray<AEnemy*> CloseEnemies; // 500m 이내
TArray<AEnemy*> MediumEnemies; // 500-1500m
TArray<AEnemy*> FarEnemies; // 1500m+
};
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(); // 거리별 재분류
}
}
: Tick 함수는 Tick Group에 따라 순차적으로 실행된다. 물리 계산 이전에 이루어져야 할 Tick과 물리 계산 이후에, 화면 출력 해줄 때 이루어져야 할 Tick이 다르기 때문이다.
[프레임 시작]
↓
TG_PrePhysics (물리 전) - 입력, 이동 명령
↓
TG_DuringPhysics (물리 엔진 계산)
↓
TG_PostPhysics (물리 후) - AI, 충돌 처리 (default)
↓
TG_PostUpdateWork (마지막) - UI, 카메라
↓
[프레임 끝]
// 플레이어 입력 - 물리 전
AMyPlayerController::AMyPlayerController()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickGroup = TG_PrePhysics;
}
// 적 AI - 물리 후 (기본값)
AEnemy::AEnemy()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickGroup = TG_PostPhysics;
}
// 카메라 - 마지막
AFollowCamera::AFollowCamera()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickGroup = TG_PostUpdateWork;
}
void AEnemy::StopDistanceCheck()
{
if (DistanceCheckTimer.IsValid())
{
GetWorld()->GetTimerManager().ClearTimer(DistanceCheckTimer);
DistanceCheckTimer.Invalidate(); // 핸들 초기화
}
}
void AEnemy::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
GetWorld()->GetTimerManager().ClearAllTimersForObject(this);
Super::EndPlay(EndPlayReason);
}
GetWorld()->GetTimerManager().SetTimer(
AttackTimer,
[this]() // 람다 캡처
{
if (ACharacter* Player = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0))
{
Player->TakeDamage(AttackDamage);
}
},
1.0f,
false
);
TWeakObjectPtr<AEnemy> WeakThis = this;
GetWorld()->GetTimerManager().SetTimer(
AttackTimer,
[WeakThis]()
{
if (WeakThis.IsValid())
{
WeakThis->PerformAttack();
}
},
2.0f,
false
);
void AEnemy::BeginPlay()
{
GetWorld()->GetTimerManager().SetTimer(
DistanceCheckTimer,
this,
&AEnemy::OnDistanceCheckTimer,
0.1f,
true
);
}
void AEnemy::OnDistanceCheckTimer()
{
CheckDistanceToPlayer();
UpdateAIState();
}
void AEnemy::Tick(float DeltaTime)
{
if (Player && Player->GetHealth() <= 0)
{
StopChasing();
PlayVictoryAnimation();
}
}
void APlayer::TakeDamage(float Damage)
{
Health -= Damage;
if (Health <= 0)
{
OnPlayerDied.Broadcast(); // 이벤트 발생!
}
}
void AEnemy::BeginPlay()
{
if (APlayer* Player = GetPlayer())
{
// 이벤트 구독
Player->OnPlayerDied.AddUObject(this, &AEnemy::OnPlayerDeath);
}
}
void AEnemy::OnPlayerDeath()
{
StopChasing();
PlayVictoryAnimation();
}
UCLASS()
class UGameEventBus : public UObject
{
GENERATED_BODY()
public:
static UGameEventBus* GetInstance();
// Enemy 관련 이벤트들
DECLARE_MULTICAST_DELEGATE_OneParam(FOnEnemyKilled, AEnemy*);
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnEnemyDamaged, AEnemy*, float);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnEnemySpawned, AEnemy*);
FOnEnemyKilled OnEnemyKilled;
FOnEnemyDamaged OnEnemyDamaged;
FOnEnemySpawned OnEnemySpawned;
private:
static UGameEventBus* Instance;
};
void AEnemy::Die()
{
// 이벤트 발생
UGameEventBus::GetInstance()->OnEnemyKilled.Broadcast(this);
Destroy();
}
void AEnemy::BeginPlay()
{
// 이벤트 구독
UGameEventBus::GetInstance()->OnEnemyKilled.AddUObject(this, &AEnemy::OnOtherEnemyKilled);
}
void AEnemy::OnOtherEnemyKilled(AEnemy* DeadEnemy)
{
// 동료가 죽으면 경계 레벨 상승
if (FVector::Dist(GetActorLocation(), DeadEnemy->GetActorLocation()) < 1000.0f)
{
AlertLevel = FMath::Min(AlertLevel + 1, MaxAlertLevel);
}
}
-> 스레드별로 할 수 있는 작업이 구분되어 있기 때문에 이를 다르게 명령할 시에 크래시 발생
-> Worker Thread의 일은 모든 스레드에서 가능한 작업
EAsyncExecution::Thread : 새 스레드 생성EAsyncExecution::ThreadPool : 기존 워커 풀 사용 (일반적)EAsyncExecution::ThreadIfForkSafe : 조건부 스레드EAsyncExecution::TaskGraph : 태스크 그래프 사용EAsyncExecution::TaskGraphMainThread : 게임 스레드로 예약// Enemy 거리순 정렬
Async(EAsyncExecution::ThreadPool, [this]()
{
SortEnemiesByDistance();
});
// Enemy 처치 후 UI 업데이트 (Game Thread 전용)
AsyncTask(ENamedThreads::GameThread, [this]()
{
EnemyCountWidget->SetText(FText::AsNumber(--RemainingEnemies));
});
// Enemy AI 계산을 백그라운드로
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this]()
{
CalculateAllEnemyNextActions();
});
#include "Tasks/Task.h"
// Enemy 스폰 → 초기화 → AI 설정 체이닝
UE::Tasks::Launch(UE_SOURCE_LOCATION,
[]() { return LoadEnemyData(); })
.Then(UE_SOURCE_LOCATION,
[](auto Data) { return InitializeEnemies(Data); })
.Then(UE_SOURCE_LOCATION,
[](auto Enemies) { SetupEnemyAI(Enemies); });
// Enemy 200마리 병렬 처리
TArray<UE::Tasks::FTask> EnemyTasks;
for (int i = 0; i < 200; ++i)
{
EnemyTasks.Add(UE::Tasks::Launch(UE_SOURCE_LOCATION, [i]()
{
return ProcessEnemyAI(i);
}));
}
UE::Tasks::WaitAll(EnemyTasks);