UE5 Subsystem 설계 패턴
UE5 Subsystem은 엔진이 자동으로 생성·소멸을 관리하는 싱글턴 서비스입니다. 기존에 GameMode나 GameInstance에 모든 기능을 몰아넣던 방식 대신, 도메인별로 Subsystem을 분리하면 모듈성과 테스트 가능성이 크게 향상됩니다.
Subsystem 종류와 생명주기
섹션 제목: “Subsystem 종류와 생명주기”| 타입 | 기반 클래스 | 생존 범위 | 접근 방법 |
|---|---|---|---|
| Engine | UEngineSubsystem | 엔진 시작~종료 | GEngine->GetEngineSubsystem<T>() |
| Editor | UEditorSubsystem | 에디터 세션 | GEditor->GetEditorSubsystem<T>() |
| GameInstance | UGameInstanceSubsystem | 게임 인스턴스 | GameInstance->GetSubsystem<T>() |
| World | UWorldSubsystem | 월드 로드~언로드 | World->GetSubsystem<T>() |
| LocalPlayer | ULocalPlayerSubsystem | 로컬 플레이어 | LocalPlayer->GetSubsystem<T>() |
GameInstance Subsystem — 글로벌 서비스
섹션 제목: “GameInstance Subsystem — 글로벌 서비스”게임 전체 세션에서 유지되는 서비스에 적합합니다 (저장 시스템, 업적, 분석 등).
#pragma once#include "Subsystems/GameInstanceSubsystem.h"#include "InventorySubsystem.generated.h"
UCLASS()class MYGAME_API UInventorySubsystem : public UGameInstanceSubsystem{ GENERATED_BODY()
public: // 자동 초기화 여부 제어 (false 반환 시 생성 안 함) virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
// 생명주기 virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override;
// 공개 API UFUNCTION(BlueprintCallable, Category = "Inventory") void AddItem(FName ItemId, int32 Count);
UFUNCTION(BlueprintPure, Category = "Inventory") int32 GetItemCount(FName ItemId) const;
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnInventoryChanged, FName, int32); FOnInventoryChanged OnInventoryChanged;
private: TMap<FName, int32> Items;};#include "InventorySubsystem.h"
bool UInventorySubsystem::ShouldCreateSubsystem(UObject* Outer) const{ // 에디터 플레이 중에만 생성 (PIE 및 실제 게임 모두 포함) return !IsRunningCommandlet();}
void UInventorySubsystem::Initialize(FSubsystemCollectionBase& Collection){ Super::Initialize(Collection); UE_LOG(LogTemp, Log, TEXT("InventorySubsystem initialized"));}
void UInventorySubsystem::Deinitialize(){ Items.Empty(); Super::Deinitialize();}
void UInventorySubsystem::AddItem(FName ItemId, int32 Count){ Items.FindOrAdd(ItemId) += Count; OnInventoryChanged.Broadcast(ItemId, Items[ItemId]);}
int32 UInventorySubsystem::GetItemCount(FName ItemId) const{ const int32* Found = Items.Find(ItemId); return Found ? *Found : 0;}World Subsystem — 월드 범위 서비스
섹션 제목: “World Subsystem — 월드 범위 서비스”레벨별 시스템(스폰 관리, 게임 규칙, 환경 시스템 등)에 적합합니다.
#pragma once#include "Subsystems/WorldSubsystem.h"#include "SpawnManagerSubsystem.generated.h"
UCLASS()class MYGAME_API USpawnManagerSubsystem : public UWorldSubsystem{ GENERATED_BODY()
public: virtual void OnWorldBeginPlay(UWorld& InWorld) override;
// 틱 활성화 virtual bool DoesSupportWorldType(const EWorldType::Type WorldType) const override { return WorldType == EWorldType::Game || WorldType == EWorldType::PIE; }
void RegisterSpawnPoint(FVector Location); AActor* SpawnEnemy(TSubclassOf<AActor> EnemyClass);
private: TArray<FVector> SpawnPoints; int32 ActiveEnemyCount = 0; static constexpr int32 MaxEnemies = 20;};void USpawnManagerSubsystem::OnWorldBeginPlay(UWorld& InWorld){ Super::OnWorldBeginPlay(InWorld); UE_LOG(LogTemp, Log, TEXT("SpawnManager ready for world: %s"), *InWorld.GetName());}
AActor* USpawnManagerSubsystem::SpawnEnemy(TSubclassOf<AActor> EnemyClass){ if (ActiveEnemyCount >= MaxEnemies || SpawnPoints.IsEmpty()) return nullptr;
UWorld* World = GetWorld(); if (!World) return nullptr;
int32 Idx = FMath::RandRange(0, SpawnPoints.Num() - 1); FActorSpawnParameters Params; Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
AActor* Enemy = World->SpawnActor<AActor>(EnemyClass, SpawnPoints[Idx], FRotator::ZeroRotator, Params);
if (Enemy) ++ActiveEnemyCount; return Enemy;}LocalPlayer Subsystem — 플레이어 서비스
섹션 제목: “LocalPlayer Subsystem — 플레이어 서비스”멀티플레이어에서 플레이어마다 독립적인 상태 관리에 적합합니다.
#pragma once#include "Subsystems/LocalPlayerSubsystem.h"#include "PlayerStatsSubsystem.generated.h"
UCLASS()class MYGAME_API UPlayerStatsSubsystem : public ULocalPlayerSubsystem{ GENERATED_BODY()
public: virtual void Initialize(FSubsystemCollectionBase& Collection) override;
void RecordKill(); void RecordDeath(); float GetKDRatio() const;
private: int32 Kills = 0; int32 Deaths = 0;};Subsystem 접근 패턴
섹션 제목: “Subsystem 접근 패턴”C++에서 접근
섹션 제목: “C++에서 접근”// GameInstance Subsystemif (UGameInstance* GI = GetGameInstance()){ if (UInventorySubsystem* Inv = GI->GetSubsystem<UInventorySubsystem>()) { Inv->AddItem(FName("Sword"), 1); }}
// World Subsystem — 월드 컨텍스트가 있는 어디서든if (UWorld* World = GetWorld()){ USpawnManagerSubsystem* SpawnMgr = World->GetSubsystem<USpawnManagerSubsystem>(); // SpawnMgr은 nullptr일 수 있으므로 항상 null 체크 if (SpawnMgr) { SpawnMgr->RegisterSpawnPoint(GetActorLocation()); }}
// LocalPlayer Subsystemif (APlayerController* PC = GetWorld()->GetFirstPlayerController()){ if (ULocalPlayer* LP = PC->GetLocalPlayer()) { UPlayerStatsSubsystem* Stats = LP->GetSubsystem<UPlayerStatsSubsystem>(); Stats->RecordKill(); }}Blueprint에서 접근
섹션 제목: “Blueprint에서 접근”Blueprint에서는 Get Game Instance Subsystem, Get World Subsystem 등의 노드로 타입을 지정해 접근합니다. UFUNCTION(BlueprintCallable)로 선언한 함수는 Blueprint에서 바로 호출 가능합니다.
의존성 순서 제어
섹션 제목: “의존성 순서 제어”Subsystem 간 의존 관계가 있을 때 Collection.InitializeDependency로 순서를 보장합니다.
void UInventorySubsystem::Initialize(FSubsystemCollectionBase& Collection){ // SaveSystem이 먼저 초기화되도록 보장 Collection.InitializeDependency<USaveSystemSubsystem>();
Super::Initialize(Collection);
// 이제 SaveSystem 사용 가능 if (USaveSystemSubsystem* Save = GetGameInstance()->GetSubsystem<USaveSystemSubsystem>()) { LoadFromSave(Save); }}설계 원칙
섹션 제목: “설계 원칙”GameMode/GameState → 게임 규칙, 승리 조건 (월드 범위)GameInstance → 세션 전체 데이터 (로그인, 매치메이킹)WorldSubsystem → 레벨별 서비스 (스폰, AI 디렉터)LocalPlayerSubsystem → 플레이어별 UI, 통계, 입력 설정EngineSubsystem → 플러그인 서비스, 에셋 캐시Subsystem은 단일 책임 원칙에 따라 도메인 하나만 담당하도록 설계합니다. 서로 참조가 필요하면 델리게이트나 인터페이스를 통해 느슨하게 결합합니다.
핵심 요약
섹션 제목: “핵심 요약”- 자동 생명주기 관리 —
Initialize/Deinitialize만 구현 - 타입 안전 접근 — 캐스팅 없이
GetSubsystem<T>() - 의존 순서 —
Collection.InitializeDependency<T>() - 멀티플레이어 분리 —
LocalPlayerSubsystem으로 플레이어별 상태 격리 - GameMode 비대화 방지 — 도메인별 Subsystem으로 분산