PART 12 - 강의 7/8

최적화 및 프로파일링

Unreal Insights와 성능 최적화 전략

01

Unreal Insights 활용

CPU/GPU 프로파일링 도구

C++
// 프로파일링을 위한 커스텀 STAT 그룹 정의 #include "Stats/Stats.h" // 커스텀 STAT 그룹 선언 DECLARE_STATS_GROUP(TEXT("OpenWorldRPG"), STATGROUP_OpenWorldRPG, STATCAT_Advanced); // 개별 STAT 선언 DECLARE_CYCLE_STAT(TEXT("Combat System Tick"), STAT_CombatSystemTick, STATGROUP_OpenWorldRPG); DECLARE_CYCLE_STAT(TEXT("AI Decision Making"), STAT_AIDecisionMaking, STATGROUP_OpenWorldRPG); DECLARE_CYCLE_STAT(TEXT("World Partition Streaming"), STAT_WorldPartitionStreaming, STATGROUP_OpenWorldRPG); DECLARE_DWORD_COUNTER_STAT(TEXT("Active Enemies"), STAT_ActiveEnemies, STATGROUP_OpenWorldRPG); DECLARE_FLOAT_COUNTER_STAT(TEXT("Memory Usage MB"), STAT_MemoryUsageMB, STATGROUP_OpenWorldRPG); // 사용 예시 void UCombatSubsystem::Tick(float DeltaTime) { // 이 블록 내 실행 시간이 측정됨 SCOPE_CYCLE_COUNTER(STAT_CombatSystemTick); // 전투 로직... ProcessCombatActions(); } void AEnemyAIController::MakeDecision() { SCOPE_CYCLE_COUNTER(STAT_AIDecisionMaking); // AI 결정 로직... } // 카운터 업데이트 void UEnemyManager::UpdateStats() { SET_DWORD_STAT(STAT_ActiveEnemies, ActiveEnemies.Num()); SET_FLOAT_STAT(STAT_MemoryUsageMB, GetMemoryUsage() / 1024.0f / 1024.0f); }
C++
// Trace 기반 프로파일링 (Unreal Insights) #include "ProfilingDebugging/CpuProfilerTrace.h" #include "ProfilingDebugging/MiscTrace.h" void AGameCharacter::PerformHeavyCalculation() { // Unreal Insights에서 보이는 스코프 이름 TRACE_CPUPROFILER_EVENT_SCOPE(GameCharacter_HeavyCalculation); // 계산 로직... for (int32 i = 0; i < 1000; ++i) { ProcessItem(i); } } void UWorldStreamingManager::StreamLevel(const FName& LevelName) { // 북마크 추가 - 타임라인에서 특정 이벤트 표시 TRACE_BOOKMARK(TEXT("Level Streaming: %s"), *LevelName.ToString()); // 스트리밍 로직... } // 카운터 추적 (메모리, 오브젝트 수 등) void UMemoryTracker::UpdateCounters() { TRACE_COUNTER_SET(TEXT("Game/LoadedActors"), LoadedActorCount); TRACE_COUNTER_ADD(TEXT("Game/SpawnedThisFrame"), NewSpawns); } // 콘솔 명령으로 트레이스 시작/종료 // trace.start cpu,gpu,frame,bookmark,memory // trace.stop // 또는 명령줄: -trace=cpu,gpu,frame,bookmark
02

메모리 최적화

LLM과 메모리 관리 전략

C++
// Low Level Memory (LLM) 태깅 #include "HAL/LowLevelMemTracker.h" // 커스텀 LLM 태그 정의 LLM_DEFINE_TAG(OpenWorldRPG); LLM_DEFINE_TAG(OpenWorldRPG_Combat); LLM_DEFINE_TAG(OpenWorldRPG_AI); LLM_DEFINE_TAG(OpenWorldRPG_Streaming); // 메모리 할당 태깅 void UCombatManager::AllocateCombatData() { // 이 스코프 내 모든 메모리 할당이 태그됨 LLM_SCOPE(ELLMTag::OpenWorldRPG_Combat); CombatData = new FCombatData[MAX_COMBAT_INSTANCES]; HitResults.Reserve(1000); } // 오브젝트 풀링으로 메모리 재사용 UCLASS() class UProjectilePool : public UObject { GENERATED_BODY() public: void Initialize(int32 PoolSize); AProjectile* GetPooledProjectile(); void ReturnToPool(AProjectile* Projectile); private: UPROPERTY() TArray<TObjectPtr<AProjectile>> AvailableProjectiles; UPROPERTY() TArray<TObjectPtr<AProjectile>> ActiveProjectiles; }; void UProjectilePool::Initialize(int32 PoolSize) { LLM_SCOPE(ELLMTag::OpenWorldRPG_Combat); AvailableProjectiles.Reserve(PoolSize); for (int32 i = 0; i < PoolSize; ++i) { AProjectile* Projectile = GetWorld()->SpawnActor<AProjectile>(); Projectile->SetActorHiddenInGame(true); Projectile->SetActorEnableCollision(false); AvailableProjectiles.Add(Projectile); } } AProjectile* UProjectilePool::GetPooledProjectile() { if (AvailableProjectiles.IsEmpty()) { // 풀 확장 또는 nullptr 반환 UE_LOG(LogTemp, Warning, TEXT("Projectile pool exhausted!")); return nullptr; } AProjectile* Projectile = AvailableProjectiles.Pop(); Projectile->SetActorHiddenInGame(false); Projectile->SetActorEnableCollision(true); ActiveProjectiles.Add(Projectile); return Projectile; } void UProjectilePool::ReturnToPool(AProjectile* Projectile) { if (!Projectile) return; Projectile->SetActorHiddenInGame(true); Projectile->SetActorEnableCollision(false); Projectile->ResetProjectile(); ActiveProjectiles.Remove(Projectile); AvailableProjectiles.Add(Projectile); }
03

CPU 최적화

멀티스레딩과 Task Graph 활용

C++
// ParallelFor를 활용한 병렬 처리 #include "Async/ParallelFor.h" void UAIManager::UpdateAllAI(float DeltaTime) { SCOPE_CYCLE_COUNTER(STAT_AIDecisionMaking); // 병렬로 AI 상태 계산 ParallelFor(AIControllers.Num(), [&](int32 Index) { if (AAIController* Controller = AIControllers[Index]) { // 읽기 전용 계산만 수행 AIDecisions[Index] = CalculateAIDecision(Controller, DeltaTime); } }); // 게임 스레드에서 결과 적용 (순차적) for (int32 i = 0; i < AIControllers.Num(); ++i) { if (AIControllers[i]) { ApplyAIDecision(AIControllers[i], AIDecisions[i]); } } } // AsyncTask로 백그라운드 작업 #include "Async/Async.h" void UPathfindingSubsystem::RequestPathAsync( const FVector& Start, const FVector& End, FOnPathFound OnComplete) { // 복잡한 경로 탐색을 백그라운드에서 AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, Start, End, OnComplete]() { // 백그라운드 스레드에서 경로 계산 TArray<FVector> Path = CalculateDetailedPath(Start, End); // 게임 스레드로 결과 전달 AsyncTask(ENamedThreads::GameThread, [OnComplete, Path]() { OnComplete.ExecuteIfBound(Path); }); }); } // Tick 최적화 - 시분할 업데이트 UCLASS() class UTimeslicedUpdater : public UWorldSubsystem { GENERATED_BODY() public: void RegisterForUpdate(IUpdateable* Object) { UpdateQueue.Add(Object); } virtual void Tick(float DeltaTime) override { // 프레임당 최대 업데이트 수 const int32 MaxUpdatesPerFrame = 50; int32 UpdateCount = 0; while (UpdateCount < MaxUpdatesPerFrame && !UpdateQueue.IsEmpty()) { IUpdateable* Object = UpdateQueue[CurrentIndex]; if (Object && Object->NeedsUpdate()) { Object->PerformUpdate(DeltaTime * UpdateQueue.Num()); ++UpdateCount; } CurrentIndex = (CurrentIndex + 1) % UpdateQueue.Num(); } } private: TArray<IUpdateable*> UpdateQueue; int32 CurrentIndex = 0; };
04

GPU 최적화

드로우콜 최소화와 LOD 시스템

C++
// 인스턴스드 스태틱 메시 컴포넌트 활용 UCLASS() class AFoliageManager : public AActor { GENERATED_BODY() public: AFoliageManager(); UFUNCTION(BlueprintCallable) void AddFoliageInstance(const FTransform& Transform); UFUNCTION(BlueprintCallable) void RemoveFoliageInstance(int32 InstanceIndex); protected: // 하나의 드로우콜로 많은 메시 렌더링 UPROPERTY(VisibleAnywhere) TObjectPtr<UInstancedStaticMeshComponent> FoliageISMC; UPROPERTY(EditDefaultsOnly) TObjectPtr<UStaticMesh> FoliageMesh; UPROPERTY(EditDefaultsOnly) int32 MaxInstances = 10000; }; AFoliageManager::AFoliageManager() { FoliageISMC = CreateDefaultSubobject<UInstancedStaticMeshComponent>( TEXT("FoliageISMC")); RootComponent = FoliageISMC; // 컬링 거리 설정 FoliageISMC->SetCullDistances(0, 50000); // Nanite 지원 여부에 따라 설정 if (FoliageMesh && FoliageMesh->HasValidNaniteData()) { // Nanite 사용 } } void AFoliageManager::AddFoliageInstance(const FTransform& Transform) { if (FoliageISMC->GetInstanceCount() < MaxInstances) { FoliageISMC->AddInstance(Transform, true); } } // LOD 시스템 커스터마이징 UCLASS() class ALODCharacter : public ACharacter { GENERATED_BODY() public: virtual void Tick(float DeltaTime) override; protected: // LOD 레벨별 설정 UPROPERTY(EditDefaultsOnly, Category = "LOD") TArray<float> LODDistances = { 1000.0f, 3000.0f, 5000.0f }; // 현재 LOD 레벨 int32 CurrentLODLevel = 0; void UpdateLOD(); void ApplyLODSettings(int32 LODLevel); }; void ALODCharacter::UpdateLOD() { // 카메라와의 거리 계산 APlayerCameraManager* CamManager = UGameplayStatics::GetPlayerCameraManager( this, 0); if (!CamManager) return; float Distance = FVector::Dist( GetActorLocation(), CamManager->GetCameraLocation() ); // LOD 레벨 결정 int32 NewLODLevel = LODDistances.Num(); for (int32 i = 0; i < LODDistances.Num(); ++i) { if (Distance < LODDistances[i]) { NewLODLevel = i; break; } } if (NewLODLevel != CurrentLODLevel) { CurrentLODLevel = NewLODLevel; ApplyLODSettings(CurrentLODLevel); } } void ALODCharacter::ApplyLODSettings(int32 LODLevel) { switch (LODLevel) { case 0: // 가까움 - 최고 품질 GetMesh()->SetForcedLOD(0); GetMesh()->bDisableClothSimulation = false; break; case 1: // 중간 GetMesh()->SetForcedLOD(1); GetMesh()->bDisableClothSimulation = true; break; case 2: // 멀리 GetMesh()->SetForcedLOD(2); break; default: // 매우 멀리 - 최소 품질 GetMesh()->SetForcedLOD(3); break; } }
SUMMARY

핵심 요약

  • SCOPE_CYCLE_COUNTER로 커스텀 STAT 측정
  • TRACE_CPUPROFILER_EVENT_SCOPE로 Insights 추적
  • LLM_SCOPE로 메모리 할당 태깅
  • ParallelFor로 병렬 계산
  • 오브젝트 풀링으로 GC 부하 감소
  • ISMC로 드로우콜 최소화
PRACTICE

도전 과제

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

실습 1: 종합 프로파일링 체크리스트 실행

stat fps, stat unit, stat GPU, stat Memory, stat SceneRendering을 체계적으로 실행하고, 각 영역의 성능 데이터를 스프레드시트에 기록하세요. 병목 지점 Top 5를 식별하세요.

실습 2: 식별된 병목 최적화

프로파일링에서 발견된 병목을 하나씩 최적화하세요. 드로우콜 줄이기(ISM), Tick 비활성화, 텍스처 해상도 축소, Nanite/HLOD 설정 조정 등을 적용하고, 전후 성능을 비교하세요.

심화 과제: 타겟 프레임레이트 달성 계획

60fps(PC) 또는 30fps(콘솔)를 목표로 전체 최적화 로드맵을 작성하세요. GPU Bound vs CPU Bound 판별, 프레임 예산 분배(렌더링 8ms, 게임 로직 4ms 등), 우선순위별 최적화 태스크를 계획하세요.