게임 로직 처리
입력 처리
UI 업데이트
UObject 생성/삭제를 할 수 있는 유일한 스레드
// Enemy 스폰
AEnemy* NewEnemy = GetWorld()->SpawnActor<AEnemy>();
// UI 조작
EnemyCountWidget->SetText(FText::AsNumber(EnemyCount));
// 컴포넌트 추가/제거
UStaticMeshComponent* Mesh = NewObject<UStaticMeshComponent>(this);
// 대부분의 언리얼 API
UGameplayStatics::GetPlayerController(GetWorld(), 0);
그래픽 담당
드로우 콜 준비
머티리얼 처리
렌더 커맨드 생성
게임 스레드보다 1프레임 뒤에 실행되어 게임 스레드가 데이터를 넘겨주면, 렌더링 스레드가 이를 받아 실제 GPU에 명령을 내림
렌더링 스레드에서 생성된 명령을 실제 그래픽 API(DirectX, Vulkan, Metal 등)에 맞게 변환하여 GPU로 전송하는 스레드
Render Thread의 부하를 덜어줌
특정 역할이 고정되지 않은 범용 스레드 집합
각 스레드 고유의 역할을 제외한 단순 작업은 어느 스레드에서 해도 상관 없어 이 작업자 스레드에서 실행
스레드들의 부하를 덜어줄 수 있다
cpu 코어 개수에 맞게 미리 생성되어 있음 (Pooling)
물리 계산, AI 경로 탐색, 파일 로딩, 기타 백그라운드 작업
그러나, 각 스레드의 고유역할을 여기서 해버리면 프로그램이 터지니 애매하면 다른 스레드로 넘기지 말 것
Main Thread에서 작업도중 무거운 작업을 Worker Thread에게 맡기는 방법
UObject나 World에 접근하는 작업은 무조건 Main Thread에서 해야함.
그 이외의 무거운 작업만 맡겼다가, 다시 받아서 사용
예) Actor, UI 등
처음부터 멀티스레드를 사용하지 말고, 꼭 프로파일링 후 필요하면 멀티스레드 사용
언리얼4부터 사용가능
ASync는 무거운 작업을 Worker Thread에 넘기는 것
ASyncTask는 특정 스레드에게 작업을 시키는 것
그래서 보통 ASync로 Worker Thread에 작업을 넘기고, Worker Thread에서 AsyncTask로 완료한 작업을 MainThread에게 보내 처리함
비동기이므로, Worker Thread는 백그라운드에서 작업을 계속 처리하고, Main Thread 역시 그 이후 작업을 하므로 게임이 멈추지 않고 계속 진행됨
#include "Async/Async.h"
// 모든 Enemy의 경로를 업데이트해주는 비동기 함수
void AEnemyManager::UpdateAllEnemyPathsAsync()
{
// 1. 모든 Enemy이 경로 데이터를 담을 SharedPtr 생성
TSharedPtr<TArray<FEnemyPathData>> SharedEnemyData =
MakeShared<TArray<FEnemyPathData>>();
for (AEnemy* Enemy : AllEnemies)
{
if (Enemy && Player)
{
FEnemyPathData Data;
Data.EnemyID = Enemy->GetUniqueID();
Data.StartLocation = Enemy->GetActorLocation();
Data.TargetLocation = Player->GetActorLocation();
SharedEnemyData->Add(Data);
}
}
// 2. Worker Thread에 작업 맡김
Async(EAsyncExecution::ThreadPool, [this, SharedEnemyData]()
{
// [Worker Thread 영역]
TArray<FEnemyPathResult> PathResults;
// 무거운 작업
// const 참조를 사용하여 읽기 전용으로 접근
for (const FEnemyPathData& Data : *SharedEnemyData)
{
FEnemyPathResult Result;
Result.EnemyID = Data.EnemyID;
Result.Path = FindPathAStar(Data.StartLocation, Data.TargetLocation);
PathResults.Add(Result);
}
// 3. 작업 완료 후, ASyncTask불러 GameThread에게 결과물로 작업시킴
AsyncTask(ENamedThreads::GameThread, [this, PathResults]()
{
// 작업 결과 적용
for (const FEnemyPathResult& Result : PathResults)
{
if (AEnemy* Enemy = FindEnemyByID(Result.EnemyID))
{
Enemy->SetPath(Result.Path);
}
}
});
});
}
비동기 작업 도중 Worker Thread로 넘긴 데이터가 사라지면 터짐
따라서 TArray 자체를 공유 포인터로 감싸, 워커 스레드가 끝날 때까지 메모리가 유지되도록 함
복사하고 스레드에 넘기기에는 데이터가 커서 복사비용으로 성능이 떨어질 수 있기 때문에, SharedPtr을 사용.
또한, Main과 Worker에서 같이 접근해 Race Condition이 발생할 수 있어 Worker에선 const 참조자를 이용해 읽기만 가능하도록 설정
EAsyncExecution::ThreadPool : 이미 만들어진 워커 풀 사용 (일반적이고 빠름). 보통 가볍고 자주 사용되는 작업에 적합
EAsyncExecution::Thread : 새 스레드를 생성. 다른 스레드에 간섭 없이 정말 무거운 작업을 혼자 처리하고 싶을 때 적합. 새로 생성하기에 좀 느림
EAsyncExecution::ThreadIfForkSafe : 조건부 스레드
EAsyncExecution::TaskGraph : 작업을 태스크 단위로 작게 나누어 병렬로(Graph) 처리
EAsyncExecution::TaskGraphMainThread : TaskGraph와 동일하나 다음 프레임에 게임 스레드를 예약
언리얼 5부터 사용 가능
작업을 맡기고 끝이 아니라, 여러 작업을 계속 이을 수 있음 (Chaining)
병렬 작업이라 빠름
#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); // EnemyTasks 작업이 끝날 때까지 대기