UE5 C++ USubsystem
개요 — Subsystem이란
섹션 제목: “개요 — Subsystem이란”USubsystem은 UE5에서 제공하는 자동 생성·자동 소멸 싱글턴 서비스 패턴입니다. GameInstance 등 특정 Owner 객체와 수명이 연동되어, 별도의 GetInstance() 패턴 없이도 어디서든 GetSubsystem<T>()로 접근할 수 있습니다.
기존의 GameInstance에 모든 서비스를 몰아 넣던 안티패턴 대신, 기능별 Subsystem으로 분리하면 코드 유지보수성이 크게 향상됩니다.
1. Subsystem 네 가지 유형
섹션 제목: “1. Subsystem 네 가지 유형”| 유형 | 기반 클래스 | 수명 | 인스턴스 수 | 주요 사용처 |
|---|---|---|---|---|
| GameInstance | UGameInstanceSubsystem | 게임 시작 ~ 종료 | 1개 | 세이브, 스코어, 글로벌 서비스 |
| World | UWorldSubsystem | 레벨 로드 ~ 언로드 | World당 1개 | 레벨별 게임 상태, AI 관리자 |
| Engine | UEngineSubsystem | 에디터/런타임 전체 | 1개 | 에디터 툴, 글로벌 에디터 서비스 |
| LocalPlayer | ULocalPlayerSubsystem | 플레이어 생성 ~ 소멸 | 플레이어당 1개 | 입력 설정, UI 상태, 분할 화면 |
2. GameInstance Subsystem
섹션 제목: “2. GameInstance Subsystem”2.1 선언
섹션 제목: “2.1 선언”#pragma once
#include "CoreMinimal.h"#include "Subsystems/GameInstanceSubsystem.h"#include "SaveGameSubsystem.generated.h"
UCLASS()class MYGAME_API USaveGameSubsystem : public UGameInstanceSubsystem{ GENERATED_BODY()
public: // USubsystem 인터페이스 virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override;
// 게임 저장/로드 API UFUNCTION(BlueprintCallable, Category = "SaveGame") void SaveGame(const FString& SlotName);
UFUNCTION(BlueprintCallable, Category = "SaveGame") bool LoadGame(const FString& SlotName);
UFUNCTION(BlueprintPure, Category = "SaveGame") int32 GetHighScore() const { return HighScore; }
private: int32 HighScore = 0; FString CurrentSlotName;};2.2 구현
섹션 제목: “2.2 구현”#include "SaveGameSubsystem.h"#include "Kismet/GameplayStatics.h"#include "MySaveGame.h"
void USaveGameSubsystem::Initialize(FSubsystemCollectionBase& Collection){ Super::Initialize(Collection); UE_LOG(LogTemp, Log, TEXT("SaveGameSubsystem initialized"));
// 자동으로 마지막 슬롯 로드 LoadGame(TEXT("DefaultSave"));}
void USaveGameSubsystem::Deinitialize(){ // 종료 시 자동 저장 SaveGame(CurrentSlotName); Super::Deinitialize();}
void USaveGameSubsystem::SaveGame(const FString& SlotName){ UMySaveGame* SaveGameObj = Cast<UMySaveGame>( UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
if (SaveGameObj) { SaveGameObj->HighScore = HighScore; UGameplayStatics::SaveGameToSlot(SaveGameObj, SlotName, 0); CurrentSlotName = SlotName; }}
bool USaveGameSubsystem::LoadGame(const FString& SlotName){ if (!UGameplayStatics::DoesSaveGameExist(SlotName, 0)) { return false; }
UMySaveGame* SaveGameObj = Cast<UMySaveGame>( UGameplayStatics::LoadGameFromSlot(SlotName, 0));
if (SaveGameObj) { HighScore = SaveGameObj->HighScore; CurrentSlotName = SlotName; return true; } return false;}2.3 접근 방법
섹션 제목: “2.3 접근 방법”// 어디서든 GameInstance Subsystem에 접근void AMyActor::SomeFunction(){ UGameInstance* GI = GetWorld()->GetGameInstance(); if (USaveGameSubsystem* SaveSys = GI->GetSubsystem<USaveGameSubsystem>()) { SaveSys->SaveGame(TEXT("Slot1")); int32 HighScore = SaveSys->GetHighScore(); }}
// PlayerController / Character에서 축약void AMyPlayerController::SaveProgress(){ if (USaveGameSubsystem* SaveSys = GetGameInstance()->GetSubsystem<USaveGameSubsystem>()) { SaveSys->SaveGame(TEXT("Auto")); }}3. World Subsystem
섹션 제목: “3. World Subsystem”레벨이 로드될 때마다 새로 생성되며, 해당 World에 종속됩니다. 레벨 전환 시 자동 소멸·재생성됩니다.
#pragma once
#include "CoreMinimal.h"#include "Subsystems/WorldSubsystem.h"#include "WaveManagerSubsystem.generated.h"
DECLARE_MULTICAST_DELEGATE_OneParam(FOnWaveStarted, int32 /*WaveNumber*/);
UCLASS()class MYGAME_API UWaveManagerSubsystem : public UWorldSubsystem{ GENERATED_BODY()
public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override;
// ShouldCreateSubsystem: 특정 조건에서만 생성 virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
void StartNextWave(); int32 GetCurrentWave() const { return CurrentWave; }
FOnWaveStarted OnWaveStarted;
private: int32 CurrentWave = 0; FTimerHandle WaveTimerHandle;
void SpawnEnemiesForWave(int32 WaveNumber);};bool UWaveManagerSubsystem::ShouldCreateSubsystem(UObject* Outer) const{ // 게임 월드에서만 생성 (에디터 월드 제외) if (!Super::ShouldCreateSubsystem(Outer)) return false;
UWorld* World = Cast<UWorld>(Outer); return World && World->IsGameWorld();}
void UWaveManagerSubsystem::Initialize(FSubsystemCollectionBase& Collection){ Super::Initialize(Collection); UE_LOG(LogTemp, Log, TEXT("WaveManagerSubsystem initialized for world: %s"), *GetWorld()->GetName());}
void UWaveManagerSubsystem::StartNextWave(){ ++CurrentWave; OnWaveStarted.Broadcast(CurrentWave); SpawnEnemiesForWave(CurrentWave);}
void UWaveManagerSubsystem::SpawnEnemiesForWave(int32 WaveNumber){ int32 EnemyCount = WaveNumber * 5; UE_LOG(LogTemp, Log, TEXT("Wave %d: spawning %d enemies"), WaveNumber, EnemyCount); // 스폰 로직...}// 접근void AMyGameMode::BeginPlay(){ Super::BeginPlay();
if (UWaveManagerSubsystem* WaveMgr = GetWorld()->GetSubsystem<UWaveManagerSubsystem>()) { WaveMgr->OnWaveStarted.AddUObject(this, &AMyGameMode::HandleWaveStarted); WaveMgr->StartNextWave(); }}4. LocalPlayer Subsystem
섹션 제목: “4. LocalPlayer Subsystem”플레이어당 하나씩 존재하며, 분할 화면(Split Screen) 환경에서 각 플레이어별 상태를 독립적으로 관리합니다.
#pragma once
#include "CoreMinimal.h"#include "Subsystems/LocalPlayerSubsystem.h"#include "PlayerSettingsSubsystem.generated.h"
UCLASS()class MYGAME_API UPlayerSettingsSubsystem : public ULocalPlayerSubsystem{ GENERATED_BODY()
public: virtual void Initialize(FSubsystemCollectionBase& Collection) override;
UFUNCTION(BlueprintCallable, Category = "Settings") void SetMouseSensitivity(float Sensitivity);
UFUNCTION(BlueprintPure, Category = "Settings") float GetMouseSensitivity() const { return MouseSensitivity; }
UFUNCTION(BlueprintCallable, Category = "Settings") void SetVolumeLevel(float Volume);
private: float MouseSensitivity = 1.0f; float VolumeLevel = 1.0f;
void LoadSettings(); void SaveSettings();};// LocalPlayer Subsystem 접근void AMyPlayerController::ApplySettings(){ if (ULocalPlayer* LP = GetLocalPlayer()) { if (UPlayerSettingsSubsystem* Settings = LP->GetSubsystem<UPlayerSettingsSubsystem>()) { Settings->SetMouseSensitivity(1.5f); } }}5. Subsystem 간 의존성 주입
섹션 제목: “5. Subsystem 간 의존성 주입”Initialize(FSubsystemCollectionBase&)의 Collection 파라미터로 다른 Subsystem이 먼저 초기화되도록 보장합니다.
void UWaveManagerSubsystem::Initialize(FSubsystemCollectionBase& Collection){ // SaveGameSubsystem이 반드시 먼저 초기화되도록 보장 Collection.InitializeDependency<USaveGameSubsystem>();
Super::Initialize(Collection);
// 이제 안전하게 SaveGameSubsystem 사용 가능 UGameInstance* GI = GetWorld()->GetGameInstance(); if (USaveGameSubsystem* SaveSys = GI->GetSubsystem<USaveGameSubsystem>()) { int32 SavedWave = SaveSys->GetHighScore(); // 예시 }}6. 접근 방법 요약
섹션 제목: “6. 접근 방법 요약”// GameInstance SubsystemUGameInstance* GI = GetWorld()->GetGameInstance();USaveGameSubsystem* SaveSys = GI->GetSubsystem<USaveGameSubsystem>();
// World SubsystemUWaveManagerSubsystem* WaveMgr = GetWorld()->GetSubsystem<UWaveManagerSubsystem>();
// LocalPlayer SubsystemULocalPlayer* LP = GetWorld()->GetFirstLocalPlayerFromController();UPlayerSettingsSubsystem* Settings = LP->GetSubsystem<UPlayerSettingsSubsystem>();
// Engine Subsystem (에디터 툴 등)UMyEditorSubsystem* EditorSys = GEngine->GetEngineSubsystem<UMyEditorSubsystem>();7. Tickable World Subsystem
섹션 제목: “7. Tickable World Subsystem”매 프레임 업데이트가 필요한 Subsystem은 FTickableGameObject를 다중 상속합니다.
#pragma once#include "Subsystems/WorldSubsystem.h"#include "Tickable.h"#include "TickableWorldSubsystem.generated.h"
UCLASS()class UObjectPoolSubsystem : public UWorldSubsystem, public FTickableGameObject{ GENERATED_BODY()
public: // UWorldSubsystem virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override;
// FTickableGameObject virtual void Tick(float DeltaTime) override; virtual TStatId GetStatId() const override { RETURN_QUICK_DECLARE_CYCLE_STAT(UObjectPoolSubsystem, STATGROUP_Tickables); } virtual bool IsTickable() const override { // Deinitialize 후 틱 방지 return !IsTemplate() && IsInitialized(); }
// 오브젝트 풀 API AActor* SpawnFromPool(TSubclassOf<AActor> ActorClass, const FTransform& Transform); void ReturnToPool(AActor* Actor);
private: TMap<TSubclassOf<AActor>, TArray<AActor*>> Pool;
void TickPool(float DeltaTime);};8. 에디터 전용 Engine Subsystem
섹션 제목: “8. 에디터 전용 Engine Subsystem”#if WITH_EDITOR#pragma once#include "EditorSubsystem.h"#include "MyEditorSubsystem.generated.h"
UCLASS()class UMyEditorSubsystem : public UEditorSubsystem{ GENERATED_BODY()
public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override;
// 에디터 메뉴에 커스텀 액션 추가 void RegisterMenuActions();
UFUNCTION(BlueprintCallable, Category = "EditorTools") void BatchRenameActors(const FString& Prefix);};#endif9. 정리
섹션 제목: “9. 정리”- Subsystem은 자동 생성·소멸로 싱글턴 수명 관리를 엔진이 책임집니다.
GameInstance Subsystem: 세션 전체 유지 → 세이브·글로벌 서비스World Subsystem: 레벨 단위 → 웨이브·AI 관리자LocalPlayer Subsystem: 플레이어 단위 → 입력·UI·설정FTickableGameObject다중 상속으로 틱이 필요한 Subsystem 구현Initialize(Collection)에서InitializeDependency<T>()로 의존 순서를 명시합니다.ShouldCreateSubsystem()을 재정의해 특정 조건(게임 월드만)에서만 생성되도록 제어합니다.
흔한 실수:
World Subsystem에서GetWorld()호출 시nullptr반환 —Initialize보다 이른 시점(CDO 등)에 호출하면 발생GameInstance Subsystem을 서버/클라이언트 구분 없이 사용 — 전용 서버는 GameInstance가 존재하지만 렌더링 없음