카테고리 없음

언리얼 C++) 언리얼 서브시스템(Subsystem) 완벽 이해 및 실전 아이템 매니저 구현

eclipse2 2026. 6. 4. 19:45

1. 서브시스템 (Subsystem) 이란?

내가 직접 만들고 NewObject나 Destroy로 관리하지 않아도, 엔진이 알아서 수명(생성/파괴)을 관리해 주는 매우 편리한 매니저 클래스입니다.

서브시스템의 3대 장점

  1. 코드의 모듈화 (깔끔함): 과거처럼 GameInstance 하나에 모든 매니저(UI, 사운드, 통계 등)를 때려 박지 않아도 됩니다.
  2. 자동 메모리 관리: 엔진이 생명주기를 책임지기 때문에 생성과 가비지 컬렉션(GC)을 직접 신경 쓸 필요가 없습니다.
  3. 플러그인 친화적: 사용자가 따로 세팅할 필요 없이 코드를 넣기만 해도 알아서 엔진 라이프사이클에 맞춰 돌아갑니다.

⏳ 서브시스템 생명주기 (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);
    }
}