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<>()로 의존성을 선언하고, 퀘스트 완료 시 업적을 자동 해제하는 연동을 구현하세요.