PART 1 · 강의 7/7

Subsystem 아키텍처

매니저 클래스를 대체하는 모듈식 시스템 설계

01

Subsystem 개요

자동 라이프사이클 관리 시스템

Subsystem은 기존의 싱글톤/매니저 패턴을 대체하는 언리얼의 공식 아키텍처입니다. 자동 생성/파괴, GC 호환, Blueprint 노출이 기본 제공됩니다.

Subsystem 타입 부모 객체 라이프타임 사용 예
UEngineSubsystem GEngine 엔진 전체 글로벌 설정, 분석
UEditorSubsystem GEditor 에디터 세션 에디터 도구
UGameInstanceSubsystem UGameInstance 게임 세션 업적, 저장 시스템
UWorldSubsystem UWorld 월드/레벨 날씨, 적 스폰
ULocalPlayerSubsystem ULocalPlayer 로컬 플레이어 플레이어별 설정
💡 Subsystem의 장점
  • 자동 라이프사이클 관리 (생성/파괴 코드 불필요)
  • GC와 완벽 호환
  • Blueprint 노출 용이
  • PIE(Play In Editor) 지원
  • 테스트/모킹 용이
02

GameInstanceSubsystem

게임 세션 동안 유지되는 시스템

AchievementSubsystem.h #pragma once #include "Subsystems/GameInstanceSubsystem.h" #include "AchievementSubsystem.generated.h" UCLASS() class MYGAME_API UAchievementSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: // 선택적: Subsystem 생성 조건 virtual bool ShouldCreateSubsystem(UObject* Outer) const override { // 예: 데디케이티드 서버에서는 생성 안 함 return !IsRunningDedicatedServer(); } // 초기화 (게임 시작 시) virtual void Initialize(FSubsystemCollectionBase& Collection) override; // 정리 (게임 종료 시) virtual void Deinitialize() override; // Blueprint 호출 가능 함수 UFUNCTION(BlueprintCallable, Category="Achievements") void UnlockAchievement(FName AchievementId); UFUNCTION(BlueprintPure, Category="Achievements") bool IsAchievementUnlocked(FName AchievementId) const; UFUNCTION(BlueprintPure, Category="Achievements") TArray<FName> GetUnlockedAchievements() const; private: void LoadAchievements(); void SaveAchievements(); UPROPERTY() TSet<FName> UnlockedAchievements; };
AchievementSubsystem.cpp #include "AchievementSubsystem.h" #include "Kismet/GameplayStatics.h" void UAchievementSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); UE_LOG(LogTemp, Log, TEXT("AchievementSubsystem Initialized")); LoadAchievements(); } void UAchievementSubsystem::Deinitialize() { SaveAchievements(); UE_LOG(LogTemp, Log, TEXT("AchievementSubsystem Deinitialized")); Super::Deinitialize(); } void UAchievementSubsystem::UnlockAchievement(FName AchievementId) { if (!UnlockedAchievements.Contains(AchievementId)) { UnlockedAchievements.Add(AchievementId); UE_LOG(LogTemp, Log, TEXT("Achievement Unlocked: %s"), *AchievementId.ToString()); // 여기서 UI 알림, 사운드 재생 등 } } bool UAchievementSubsystem::IsAchievementUnlocked(FName AchievementId) const { return UnlockedAchievements.Contains(AchievementId); }
03

WorldSubsystem

월드/레벨별로 존재하는 시스템

WeatherSubsystem.h UENUM(BlueprintType) enum class EWeatherType : uint8 { Clear, Cloudy, Rainy, Stormy, Snowy }; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWeatherChanged, EWeatherType, NewWeather); UCLASS() class UWeatherSubsystem : public UWorldSubsystem { GENERATED_BODY() public: // 월드가 시작될 때 호출 virtual void OnWorldBeginPlay(UWorld& InWorld) override; // 틱 가능 virtual bool DoesSupportWorldType(const EWorldType::Type WorldType) const override { // Game 월드에서만 생성 return WorldType == EWorldType::Game || WorldType == EWorldType::PIE; } UFUNCTION(BlueprintCallable, Category="Weather") void SetWeather(EWeatherType NewWeather); UFUNCTION(BlueprintPure, Category="Weather") EWeatherType GetCurrentWeather() const { return CurrentWeather; } // Blueprint에서 바인딩 가능한 이벤트 UPROPERTY(BlueprintAssignable, Category="Weather") FOnWeatherChanged OnWeatherChanged; private: EWeatherType CurrentWeather = EWeatherType::Clear; void ApplyWeatherEffects(); }; // 구현 void UWeatherSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); // 초기 날씨 설정 SetWeather(EWeatherType::Clear); } void UWeatherSubsystem::SetWeather(EWeatherType NewWeather) { if (CurrentWeather != NewWeather) { CurrentWeather = NewWeather; ApplyWeatherEffects(); OnWeatherChanged.Broadcast(NewWeather); } }
04

Subsystem 접근 방법

다양한 컨텍스트에서 Subsystem 얻기

C++ // ===== GameInstanceSubsystem 접근 ===== // 방법 1: GameInstance에서 UGameInstance* GI = GetGameInstance(); UAchievementSubsystem* AchievementSS = GI->GetSubsystem<UAchievementSubsystem>(); // 방법 2: Actor에서 if (UGameInstance* GI = GetWorld()->GetGameInstance()) { UAchievementSubsystem* SS = GI->GetSubsystem<UAchievementSubsystem>(); SS->UnlockAchievement(TEXT("FirstKill")); } // ===== WorldSubsystem 접근 ===== UWeatherSubsystem* WeatherSS = GetWorld()->GetSubsystem<UWeatherSubsystem>(); // ===== LocalPlayerSubsystem 접근 ===== ULocalPlayer* LP = GetWorld()->GetFirstLocalPlayerFromController(); UMyPlayerSubsystem* PlayerSS = LP->GetSubsystem<UMyPlayerSubsystem>(); // ===== EngineSubsystem 접근 ===== UMyEngineSubsystem* EngineSS = GEngine->GetEngineSubsystem<UMyEngineSubsystem>(); // ===== null 체크 (ShouldCreateSubsystem 오버라이드 시 중요) ===== if (UAchievementSubsystem* SS = GetGameInstance()->GetSubsystem<UAchievementSubsystem>()) { SS->UnlockAchievement(TEXT("LevelUp")); }

Blueprint에서 접근

Blueprint // Get Game Instance -> Get Subsystem (Class: AchievementSubsystem) // 또는 // Get World -> Get Subsystem (Class: WeatherSubsystem)
05

Subsystem vs 싱글톤

왜 Subsystem을 사용해야 하는가

싱글톤 패턴의 문제점 // ❌ 전통적인 싱글톤 - 문제가 많음 class UBadSingleton : public UObject { static UBadSingleton* Instance; public: static UBadSingleton* Get() { if (!Instance) { Instance = NewObject<UBadSingleton>(); Instance->AddToRoot(); // GC 방지 - 메모리 누수? } return Instance; } }; // 문제점: // 1. PIE에서 여러 세션 공유 문제 // 2. 에디터 재시작 시 댕글링 포인터 // 3. GC와 충돌 // 4. 테스트 어려움 // 5. 라이프사이클 관리 수동

싱글톤 단점

  • PIE 다중 세션 문제
  • GC 충돌 가능
  • 수동 라이프사이클
  • 테스트/모킹 어려움

Subsystem 장점

  • PIE 완벽 지원
  • GC 완벽 호환
  • 자동 라이프사이클
  • 쉬운 테스트/모킹
06

실전 예제: 퀘스트 시스템

GameInstanceSubsystem으로 퀘스트 관리

QuestSubsystem.h USTRUCT(BlueprintType) struct FQuestData { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) FName QuestId; UPROPERTY(EditAnywhere, BlueprintReadWrite) FText QuestName; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 CurrentProgress = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 RequiredProgress = 1; bool IsComplete() const { return CurrentProgress >= RequiredProgress; } }; DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnQuestProgressChanged, FName, QuestId, int32, NewProgress); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnQuestCompleted, FName, QuestId); UCLASS() class UQuestSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; UFUNCTION(BlueprintCallable, Category="Quest") void StartQuest(const FQuestData& Quest); UFUNCTION(BlueprintCallable, Category="Quest") void UpdateQuestProgress(FName QuestId, int32 Progress); UFUNCTION(BlueprintPure, Category="Quest") bool GetQuestData(FName QuestId, FQuestData& OutData) const; UFUNCTION(BlueprintPure, Category="Quest") TArray<FQuestData> GetActiveQuests() const; UPROPERTY(BlueprintAssignable) FOnQuestProgressChanged OnQuestProgressChanged; UPROPERTY(BlueprintAssignable) FOnQuestCompleted OnQuestCompleted; private: UPROPERTY() TMap<FName, FQuestData> ActiveQuests; };
SUMMARY

핵심 요약

  • Subsystem 타입 — Engine, Editor, GameInstance, World, LocalPlayer 선택
  • 자동 라이프사이클 — Initialize/Deinitialize 자동 호출
  • ShouldCreateSubsystem — 조건부 생성 (서버/클라이언트 분리 등)
  • GetSubsystem<T>() — 부모 객체에서 접근
  • 싱글톤 대체 — PIE 지원, GC 호환, 테스트 용이
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: RPG 퀘스트 서브시스템 구현

UGameInstanceSubsystem을 상속하는 UQuestSubsystem을 만들고, Initialize/Deinitialize를 오버라이드하세요. StartQuest(), UpdateProgress(), GetActiveQuests()를 BlueprintCallable로 노출하세요.

실습 2: 날씨 월드 서브시스템 구현

UWorldSubsystem을 상속하는 UWeatherSubsystem을 만들어 오픈월드 RPG의 날씨를 관리하세요. DoesSupportWorldType()으로 Game/PIE에서만 생성되도록 하고, OnWeatherChanged 델리게이트를 BlueprintAssignable로 노출하세요.

심화 과제: 서브시스템 간 의존성 관리

UQuestSubsystem이 UAchievementSubsystem에 의존하도록 구현하세요. Initialize()에서 FSubsystemCollectionBase::InitializeDependency<>()로 의존성을 선언하고, 퀘스트 완료 시 업적을 자동 해제하는 연동을 구현하세요.