概要
動作確認環境: UE 5.4+
「機能が増えるたびにGameInstanceクラスが肥大化する…」「シングルトンの初期化順で毎回バグが出る…」――これらはUE開発で非常にありがちな問題です。
UEの Subsystem は、エンジンクラスをオーバーライドせずに機能をモジュール化するための公式の仕組みです。ライフサイクル管理(生成・破棄)はエンジンが自動で行うため、開発者はビジネスロジックの実装に集中できます。従来のシングルトンパターンと比べて、初期化順の問題やグローバル状態の管理から解放されるのが大きな利点です。
Subsystemのメリット
すべてのSubsystemは USubsystem を基底クラスとし、Initialize() と Deinitialize() が自動的に呼ばれます。さらに ShouldCreateSubsystem() をオーバーライドすれば、特定の条件下でのみインスタンスを生成するといった制御も可能です。
主なメリットをまとめます。
- シングルトン管理が不要: 定型的な初期化・終了処理を記述する必要がなく、エンジンがライフサイクルを管理する
- エンジンクラスの肥大化を防ぐ:
UGameInstanceやUWorldに直接機能を追加しなくてよいため、クラスの責務が明確になる - Blueprint/Pythonから簡単アクセス:
UFUNCTIONマクロを付けるだけで、デザイナーやスクリプターがBlueprintから利用可能 - モジュール性の向上: 機能を論理的な単位でカプセル化でき、テストや再利用がしやすくなる
なお、UEにはゲーム開発向けの3種のほかに、UEngineSubsystem(エンジンのライフタイム)と UEditorSubsystem(エディタのライフタイム)も存在しますが、本記事ではゲームプレイで使用頻 度の高い3種に絞って解説します。
ゲーム開発向け3種の比較
| Subsystem | 基底クラス | スコープ | 生存期間 |
|---|---|---|---|
| GameInstance | UGameInstanceSubsystem | ゲーム全体 | 起動〜終了(レベル遷移を跨ぐ) |
| World | UWorldSubsystem | ワールド(レベル) | レベルロード〜アンロード |
| LocalPlayer | ULocalPlayerSubsystem | ローカルプレイヤー | プレイヤー作成〜破棄 |
どのSubsystemを選ぶかは、「その機能がどの範囲で必要か」で決まります。以下にそれぞれの特徴と典型的な使い方を説明します。
GameInstance Subsystem
ゲームセッション全体で単一のインスタンスとして存在します。メニュー→ゲームプレイ→メニューとレベルを遷移しても、インスタンスは破棄されません。ゲーム全体を通じて永続的に動作するサービスに最適です。
典型的な使用例: セーブ/ロード管理、統計・実績システム、ネットワークセッション管理
UGameInstance* GI = GetGameInstance();
UMyStatsSubsystem* Stats = GI->GetSubsystem<UMyStatsSubsystem>();
World Subsystem
各 UWorld(レベル)ごとにインスタンス化され、レベルのアンロード時に自動破棄されます。あるレベルでだけ必要な機能を、GameModeやLevelBlueprintを汚染せずに分離できます。
典型的な使用例: 敵スポーン管理、レベル固有のパズルロジック、環境エフェクト制御
UWorld* World = GetWorld();
UMySpawnSubsystem* Spawner = World->GetSubsystem<UMySpawnSubsystem>();
LocalPlayer Subsystem
各ローカルプレイヤーごとにインスタンス化されます。分割画面マルチプレイヤーのように複数のローカルプレイヤーが存在する場面で、プレイヤーごとに異なるサービスを提供できます。
典型的な使用例: プレイヤー別のUI設定、入力マッピング、チュートリアル進行管理
ULocalPlayer* LP = GetLocalPlayer();
UMyUISubsystem* UI = LP->GetSubsystem<UMyUISubsystem>();
C++実装例
ここでは UGameInstanceSubsystem を継承した統計管理Subsystemの実装例を示します。他のSubsystem(World、LocalPlayer)も同様のパターンで実装できます。
// MyGameStatsSubsystem.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "MyGameStatsSubsystem.generated.h"
UCLASS()
class UMyGameStatsSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// Blueprintから呼び出せる関数
UFUNCTION(BlueprintCallable, Category = "Stats")
void IncrementStat(FName StatName, int32 Value = 1);
UFUNCTION(BlueprintPure, Category = "Stats")
int32 GetStat(FName StatName) const;
private:
TMap<FName, int32> GameStats;
};
// MyGameStatsSubsystem.cpp
#include "MyGameStatsSubsystem.h"
void UMyGameStatsSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
GameStats.Add(TEXT("Kills"), 0);
GameStats.Add(TEXT("Deaths"), 0);
}
void UMyGameStatsSubsystem::Deinitialize()
{
GameStats.Empty();
Super::Deinitialize();
}
void UMyGameStatsSubsystem::IncrementStat(FName StatName, int32 Value)
{
if (int32* Current = GameStats.Find(StatName))
{
*Current += Value;
}
}
int32 UMyGameStatsSubsystem::GetStat(FName StatName) const
{
const int32* Value = GameStats.Find(StatName);
return Value ? *Value : 0;
}
Initialize では初期データのセットアップを行い、Deinitialize でリソースを解放しています。この2つのメソッドはエンジンが自動的に呼び出すため、開発者が手動で初期化タイミングを制御する必要はありません。
LocalPlayerSubsystemの例
LocalPlayerSubsystemもほぼ同じパターンで実装できます。分割画面マルチプレイヤーでプレイヤーごとのUI設定を管理する例です。
// MyUISettingsSubsystem.h
UCLASS()
class UMyUISettingsSubsystem : public ULocalPlayerSubsystem
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "UI")
void SetHUDScale(float Scale) { HUDScale = FMath::Clamp(Scale, 0.5f, 2.0f); }
UFUNCTION(BlueprintPure, Category = "UI")
float GetHUDScale() const { return HUDScale; }
private:
float HUDScale = 1.0f;
};
ポイント:
ShouldCreateSubsystem(UObject* Outer)をオーバーライドすると、特定のGameInstance型やWorld型の場合のみSubsystemを生成するといった条件分岐が可能です。
Blueprintからのアクセス
UFUNCTION(BlueprintCallable) を付与した関数は、Blueprintから直接呼び出せます。任意のBlueprintクラスで Get Game Instance Subsystem ノード(WorldやLocalPlayerも同様のノードあり)を検索し、型に UMyGameStatsSubsystem を指定すれば、IncrementStat や GetStat がノードとして表示されます。
デザイナーがBlueprint上から統計データを操作でき、プログラマーとのワークフロー分離にも役立ちます。
ベストプラクティス
- スコープで選ぶ: ゲーム全体で必要→GameInstance、レベル単位→World、プレイヤー単位→LocalPlayer。適切なスコープを選ぶことで、不要なリソース消費を防げます
- 依存関係は最小限に: Subsystem間で直接参照し合うのは避け、イベントディスパッチャーやインターフェースで疎結合に保つのが理想です
- Tickは慎重に: SubsystemでTickを実装すると毎フレーム呼ばれます。Tick不要の場合は実装しないことでパフォーマンスを維持しましょう。Tickが必要な場合は
UTickableWorldSubsystemを継承すると、Tick関数とGetStatId()が用意されたWorldSubsystemを簡潔に実装できます - 依存関係の解決: Subsystem間に初期化順の依存がある場合、
Initialize内でCollection.InitializeDependency<UOtherSubsystem>()を呼ぶと、指定したSubsystemが先に初期化されることを保証できます - テスト容易性: 機能がSubsystemとしてカプセル化されていると、単体テストで特定機能だけを検証しやすくなります
まとめ
- Subsystem はシングルトンパターンなしで機能をモジュール化するUE公式の仕組み
- GameInstance / World / LocalPlayer の3種を、機能のスコープに応じて使い分ける
Initialize()/Deinitialize()でライフサイクルが自動管理されるUFUNCTIONマクロでBlueprintから簡単にアクセス可能- EngineやEditorのSubsystemも存在するが、ゲームプレイでは上記3種が主要