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 등), 우선순위별 최적화 태스크를 계획하세요.