1. 서브시스템 (Subsystem) 이란?
내가 직접 만들고 NewObject나 Destroy로 관리하지 않아도, 엔진이 알아서 수명(생성/파괴)을 관리해 주는 매우 편리한 매니저 클래스입니다.
서브시스템의 3대 장점
- 코드의 모듈화 (깔끔함): 과거처럼 GameInstance 하나에 모든 매니저(UI, 사운드, 통계 등)를 때려 박지 않아도 됩니다.
- 자동 메모리 관리: 엔진이 생명주기를 책임지기 때문에 생성과 가비지 컬렉션(GC)을 직접 신경 쓸 필요가 없습니다.
- 플러그인 친화적: 사용자가 따로 세팅할 필요 없이 코드를 넣기만 해도 알아서 엔진 라이프사이클에 맞춰 돌아갑니다.
⏳ 서브시스템 생명주기 (Lifecycle) 종류
| 종류 | 상속 받는 클래스 | 태어나는 시점 (Initialize) | 사라지는 시점 (Deinitialize) | Outer (주인) |
| Engine | UEngineSubsystem | 프로그램(엔진) 실행 시 | 프로그램 종료 시 | GEngine |
| Editor | UEditorSubsystem | 에디터 켜질 때 | 에디터 꺼질 때 | - |
| GameInstance | UGameInstanceSubsystem | 게임 시작 시 | 게임 종료 시 | UGameInstance |
| World (UE5) | UWorldSubsystem | 레벨(맵) 로드 시 | 레벨 변경/언로드 시 | UWorld |
| LocalPlayer | ULocalPlayerSubsystem | 플레이어 접속 시 | 플레이어 나갈 때 | ULocalPlayer |
2. 서브시스템의 제어와 관리
① 초기화와 해제 (Initialize / Deinitialize)
- Initialize: 객체 생성 직후 호출. (변수 세팅, 델리게이트 바인딩 등)
- Deinitialize: 객체 파괴 직전 호출. (메모리 해제, 언바인딩, 파일 세이브 등)
② 특정 조건에서만 생성하기 (ShouldCreateSubsystem)
모든 맵에서 다 켜지는 것이 아니라, 특정 보스 맵이나 서버 전용으로만 켜야 할 때 유용합니다.
C++
bool UMyWorldSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
// Outer는 나를 관리하는 주인 (WorldSubsystem이면 UWorld)
UWorld* World = Cast<UWorld>(Outer);
// 맵 이름에 'StarterMap'이 포함될 때만 이 서브시스템을 생성!
return (World && World->GetMapName().Contains(TEXT("StarterMap")));
}
③ 서브시스템 가져오기
복잡한 형변환(Cast) 없이 템플릿 함수로 매우 직관적으로 가져올 수 있습니다.
C++
// 블루프린트에서도 Get World Subsystem 노드 하나로 깔끔하게 호출 가능!
UMyWorldSubsystem* MySub = GetWorld()->GetSubsystem<UMyWorldSubsystem>();
3. 서브시스템 간의 생성 순서 보장 (의존성 주입)
날씨 서브시스템이 시간 서브시스템을 참조해야 하는데, 날씨가 먼저 생성되면 크래시가 납니다. 이때 Collection.InitializeDependency를 사용하여 선행 초기화를 강제할 수 있습니다.
C++
void UMyWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// "TimeWorldSubsystem이 무조건 나보다 먼저 준비되게 해줘!" (안전장치)
UTimeWorldSubsystem* TimeSub = Collection.InitializeDependency<UTimeWorldSubsystem>();
if (TimeSub) {
int32 CurrentHour = TimeSub->GetCurrentHour(); // 이제 안전하게 호출 가능
}
}
4. 실전: ItemManager 서브시스템 만들기
게임 전체에서 쓰일 아이템 비동기 로드를 전담하는 UGameInstanceSubsystem 매니저를 만듭니다.
① ItemManagerSubsystem (비동기 로드 관리자)
- RequestAsyncLoadItem: 에셋 매니저를 통해 비동기 로드를 요청하고, 완료 전까지 메모리에서 날아가지 않도록 반환된 핸들을 LoadingHandles 맵에 보관(GC 방지)합니다.
- OnLoadCompleted: 로드가 끝나면 델리게이트(OnItemLoaded.Broadcast)를 통해 "아이템 도착 완료!" 방송을 때립니다.
- UnloadItem: 쓸모없어진 아이템은 맵에서 핸들을 제거(Remove)하여 가비지 컬렉터가 치울 수 있게 만들어 줍니다.
C++
// 비동기 로드 요청의 핵심 코드
TSharedPtr<FStreamableHandle> Handle = Manager.LoadPrimaryAsset(
AssetId,
TArray<FName>(),
FStreamableDelegate::CreateUObject(this, &UItemManagerSubsystem::OnLoadCompleted, AssetId)
);
LoadingHandles.Add(AssetId, Handle); // 핸들 꽉 쥐고 있기!
② PlayerCharacter (아이템 장착)
캐릭터는 자신이 직접 무거운 에셋을 로드하지 않고, 아이템 매니저에게 "이거 로드해 줘!"라고 외주를 맡깁니다.
C++
void AItemManagerTestCharacter::EquipItem(FPrimaryAssetId ItemId)
{
UItemManagerSubsystem* ItemManager = GetGameInstance()->GetSubsystem<UItemManagerSubsystem>();
// 1. 매니저의 방송 채널(델리게이트)에 내 콜백 함수 연결
ItemManager->OnItemLoaded.AddUniqueDynamic(this, &AItemManagerTestCharacter::OnEquipReady);
// 2. 로드 요청 (외주 맡기기)
ItemManager->RequestAsyncLoadItem(ItemId);
}
void AItemManagerTestCharacter::OnEquipReady(FPrimaryAssetId LoadedId)
{
if (LoadedId != ItemToEquip) return; // 내가 요청한 게 맞는지 확인
// 3. 로드 완료되었으니 매니저 창고에서 가져와서 장착!
UItemManagerSubsystem* ItemManager = GetGameInstance()->GetSubsystem<UItemManagerSubsystem>();
UMyItemDataAsset* Data = ItemManager->GetItemData(LoadedId);
if (Data) {
CurrentEquippedItem = Data; // 장착 완료!
// 4. 볼일 끝났으니 구독 취소 (메모리 절약)
ItemManager->OnItemLoaded.RemoveDynamic(this, &AItemManagerTestCharacter::OnEquipReady);
}
}