【Unreal Engine】Subsystemの使い分け:5種類の生存期間とスコープ

作成: 2026-02-07最終更新: 2026-07-20

UEのSubsystemを5種類の生存期間で図解。GameInstance・World・LocalPlayer・Engine・Editorの違い、シングルトンやGameInstance直書きと比べた利点、Blueprintからの取得ノード、レベルをまたぐスコア管理の実践まで。

セーブデータ、設定値、ステージをまたいで持ち越すスコア。どこからでも触りたいけれど、どのアクターに置いても収まりが悪いものがあります。とりあえずGameInstanceに変数を足していくと、半年後には何十個もの変数が並んだクラスができあがります。

UEには、こうした機能のための置き場所が用意されています。 Subsystem です。この記事では、5種類のSubsystemの生存期間の違いと、シングルトンやGameInstance直書きと比べた利点、Blueprintからの使い方、そしてレベルをまたぐスコア管理を組む手順を解説します。

5つの棚が並び、それぞれ長さの違う横棒(生存期間)を持っている図。Subsystemのイメージ

この記事でわかること

  • Subsystemの正体= エンジンが自動で作って壊してくれる、機能ごとの置き場所
  • 5種類の生存期間 の違いと選び方
  • GameInstance Subsystem が最もよく使う理由
  • シングルトン自作・GameInstance直書きと比べた 具体的な利点
  • Blueprintから使う手順(Getノードで取り出す)
  • 実践: レベルをまたいで持ち越すスコアを管理する

Sponsored

なぜ必要か:置き場所に困るものがある

ゲームを作っていると、どのアクターにも属さない機能が出てきます。

  • 累計スコアや所持金のように、 レベルを移動しても消えてほしくない値
  • 音量やキーコンフィグのように、 どの画面からも読み書きしたい設定
  • セーブ・ロードのように、 1つだけあれば十分な仕組み

置き場所として真っ先に思いつくのがGameInstanceですが、ここに全部を足していくと、無関係な機能が1つのクラスに同居します。スコアの処理を直したいのに開くファイルには音量もセーブも入っている、という状態です。まずBPだけでGameInstanceに値を持たせる方法は Game Instanceでデータを持ち越す にまとめてあり、その置き場所が散らかってきたときの次の一手が、この記事のSubsystemです。

左:GameInstanceの箱にスコア・音量・セーブ・実績が詰め込まれて溢れている。右:4つの小さな箱に分かれてGameInstanceの下にぶら下がっている

Subsystemは、この分割をエンジンの仕組みとして提供します。 GameInstanceを一行も書き換えずに、機能ごとの箱を後から足せる のが本質です。箱の生成と破棄はエンジンが行うため、こちらは中身だけを書きます。


5種類の生存期間

Subsystemは5種類あり、違いは どこにぶら下がり、いつからいつまで生きるか だけです。書き方はどれも同じなので、選ぶときはこの1点を見ます。

5本の横棒の長さで生存期間を比較する図。Engineが最も長く、Editor、GameInstance、LocalPlayer、Worldの順に短くなる。Worldだけレベル遷移で切れている
種類継承する基底クラス生きている期間向いているもの
GameInstanceUGameInstanceSubsystemゲーム起動から終了まで(レベル遷移を跨ぐ)スコア、設定、セーブ管理、実績
WorldUWorldSubsystemレベルのロードからアンロードまで敵のスポーン管理、そのレベル専用の進行
LocalPlayerULocalPlayerSubsystemプレイヤー追加から削除までプレイヤーごとのUI設定、入力の割り当て
EngineUEngineSubsystemエンジンの起動から終了までエンジン全体で1つの低レベルな仕組み
EditorUEditorSubsystemエディタの起動から終了までエディタ拡張、制作ツール

ゲームプレイで使うのは、上の3つです。中でも GameInstance Subsystemが最もよく使われます 。レベルを移動しても壊されないという性質が、「持ち越したいもの」の要件とそのまま一致するためです。

判断はこの順で考えると迷いません。

  1. レベルを移動しても残ってほしいか → はいなら GameInstance
  2. そのレベルの中だけで完結するか → はいなら World
  3. プレイヤーごとに別々に持ちたいか → はいなら LocalPlayer

1本のローグライトで並べるとイメージしやすくなります。ラン中ずっと持ち越す所持金は GameInstance 、いま入っている階の敵スポーン管理は World 、画面分割の2P側だけに効くUI設定は LocalPlayer 。同じゲームの中でも、残る長さで置き場所が分かれます。

補足: Engine SubsystemとEditor Subsystemは、ゲームの中身ではなくエディタ側の道具を作るときに使います。ゲームプレイの機能を置くと、パッケージしたビルドで意図しない挙動になることがあるので、最初は上の3つだけを見ておけば十分です。

Sponsored

シングルトン自作と比べて何が良いのか

「どこからでも触れる1つだけの置き場所」は、自分でシングルトンを書いても実現できます。それでもSubsystemを使う理由は、面倒な部分をエンジンが引き受けてくれるからです。

気にすること自作シングルトンSubsystem
いつ作るか自分で書く。初期化順のバグが出やすいエンジンが Initialize() を呼ぶ
いつ壊すか自分で書く。消し忘れると値が次回起動に残るエンジンが Deinitialize() を呼ぶ
PIE(エディタ実行)での状態前回の実行の値が残ることがあるGameInstanceごとに作り直される
Blueprintから使う取得用の関数を自分で用意する専用のGetノードが最初から出る
依存の順序手作業で調整Collection.InitializeDependency<T>() で指定できる
左:自作シングルトンは作る・壊すの札を自分で持ち、PIE2回目に前回の値が残っている。右:Subsystemはエンジンの手がInitializeとDeinitializeを持ち、毎回まっさらに作り直される

特に効くのが PIEでの作り直し です。自作のstaticな置き場所は、エディタで実行を止めても値が残り、次の実行に持ち越されることがあります。「2回目のPlayだけ挙動がおかしい」という追いにくいバグの原因になります。Subsystemはこの心配がありません。

GameInstanceに直書きする方法と比べた場合の利点も同じところにあります。 機能が増えても、増えるのはファイルであってGameInstanceの行数ではない という点です。


C++で作る

Subsystemの作成にはC++が必要です。 Tools > New C++ Class... を開き、親クラスの選択で All Classes の検索欄に GameInstanceSubsystem と入力して選びます(C++クラスの作り方は第一歩の記事を参照)。

// ScoreSubsystem.h
#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "ScoreSubsystem.generated.h"

UCLASS(BlueprintType)
class MYPROJECT_API UScoreSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    // エンジンが自動で呼ぶ。コンストラクタ代わりに使う
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;

    UFUNCTION(BlueprintCallable, Category = "Score")
    void AddScore(int32 Amount);

    UFUNCTION(BlueprintPure, Category = "Score")
    int32 GetTotalScore() const { return TotalScore; }

private:
    int32 TotalScore = 0;
};
// ScoreSubsystem.cpp
#include "ScoreSubsystem.h"

void UScoreSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
    TotalScore = 0;
}

void UScoreSubsystem::Deinitialize()
{
    Super::Deinitialize();
}

void UScoreSubsystem::AddScore(int32 Amount)
{
    TotalScore += Amount;
}

3つだけ押さえておきます。

  • Initialize()コンストラクタ代わり です。初期値の設定や他システムへの登録はここに書きます
  • Deinitialize() は片付け用です。何も持っていなければ空でも構いません
  • ShouldCreateSubsystem(UObject* Outer) をオーバーライドすると、 作るかどうかを条件で決められます 。特定の場面でだけ必要なSubsystemに使います

C++からの取り出しは1行です。

if (UGameInstance* GI = GetGameInstance())
{
    UScoreSubsystem* Score = GI->GetSubsystem<UScoreSubsystem>();
    Score->AddScore(100);
}
Sponsored

Blueprintから使う

作成にC++が必要というだけで、 使う側はBlueprintだけで完結します 。ここを知らずにC++でしか触れないと思い込んでいる人が多い部分です。

Blueprintグラフ上に「Get Score Subsystem」ノードが置かれ、そこからAdd Scoreへ線が伸びている図

イベントグラフで右クリックし、クラス名で検索します。UScoreSubsystem なら Get Score Subsystem というノードが出てきます。青い参照ピンを引き出せば、BlueprintCallable を付けた関数がそのまま並びます。

BP_Enemy(敵を倒したとき)
Event Destroyed
  → Get Score Subsystem
  → Add Score(Amount: 100)

WBP_HUD(表示側・まずは取得できることの確認)
Event Construct → Get Score Subsystem → Get Total Score → Set Text(現在値を1回表示)

表示側を Event Tick で毎フレーム読みたくなりますが、ここでは避けます。スコアは変わったときだけ更新すれば十分なので、実践では Event Tick ではなく、スコアが変わった瞬間に通知を受ける形(Bind)にします。

汎用ノードの Get Game Instance Subsystem もあり、こちらは Class ピンにSubsystemの型を指定して使います。World用・LocalPlayer用の同種のノードも用意されています。

補足: ノードが検索に出てこない場合は、クラスに UCLASS(BlueprintType) が付いているかと、ビルドが通っているかを確認してください。ヘッダーを変更したあとはLive Codingでは反映されないため、エディタを閉じてビルドし直します。


実践:レベルをまたぐスコアを管理する

ステージ制シューティングの累計スコア、ローグライクの1回のラン中の所持金、レースゲームのシリーズ通算ポイント。「レベルが変わっても数字を持ち越したい」場面は、どのジャンルにもあります。

これをGameModeやレベルBlueprintに書くと、レベルを移動した瞬間に0へ戻ります。GameModeはレベルごとに作り直されるためです(→ ゲームフレームワークの記事)。GameInstance Subsystemに置けば、この問題が消えます。

完成形

Level1で300点を取り、Open LevelでLevel2へ移動しても表示が300のまま続き、Level2で追加した100点が400になる流れ図。下に「GameModeに置いた場合は0に戻る」の比較

再現条件

UScoreSubsystem(親クラス:UGameInstanceSubsystem)に、次のメンバを用意します。

メンバ名初期値指定子
TotalScoreint320private(BPには出さない)
HighScoreint320private
OnScoreChangedFOnScoreChangedSignatureint32 1つ)BlueprintAssignable
AddScore(int32 Amount)戻り値なしBlueprintCallable
ResetRun()戻り値なしBlueprintCallable
GetTotalScore()int32 を返す const 関数BlueprintPure

TotalScore を直接公開せず、AddScore を通してのみ変更できるようにしているのが要点です。値を書き換えられる場所が1箇所に固定されるので、「どこかで勝手に0にされている」を追わずに済みます。

C++側:完成コード

// ScoreSubsystem.h
#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "ScoreSubsystem.generated.h"

// スコアが変わったことを知らせるデリゲート
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnScoreChangedSignature, int32, NewTotalScore);

UCLASS(BlueprintType)
class MYPROJECT_API UScoreSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;

    // BlueprintからBindできるイベント
    UPROPERTY(BlueprintAssignable, Category = "Score")
    FOnScoreChangedSignature OnScoreChanged;

    // 加点する。マイナスや0は無視する
    UFUNCTION(BlueprintCallable, Category = "Score")
    void AddScore(int32 Amount);

    // 1プレイの開始時に呼ぶ。ハイスコアは残す
    UFUNCTION(BlueprintCallable, Category = "Score")
    void ResetRun();

    UFUNCTION(BlueprintPure, Category = "Score")
    int32 GetTotalScore() const { return TotalScore; }

    UFUNCTION(BlueprintPure, Category = "Score")
    int32 GetHighScore() const { return HighScore; }

private:
    int32 TotalScore = 0;
    int32 HighScore = 0;
};
// ScoreSubsystem.cpp
#include "ScoreSubsystem.h"

void UScoreSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);

    TotalScore = 0;
    HighScore = 0;

    UE_LOG(LogTemp, Log, TEXT("ScoreSubsystem initialized."));
}

void UScoreSubsystem::Deinitialize()
{
    UE_LOG(LogTemp, Log, TEXT("ScoreSubsystem deinitialized. High: %d"), HighScore);

    Super::Deinitialize();
}

void UScoreSubsystem::AddScore(int32 Amount)
{
    if (Amount <= 0)
    {
        return;
    }

    TotalScore += Amount;

    if (TotalScore > HighScore)
    {
        HighScore = TotalScore;
    }

    UE_LOG(LogTemp, Log, TEXT("Score +%d -> Total: %d"), Amount, TotalScore);

    // 表示側へ通知する。誰が聴いているかは知らない
    OnScoreChanged.Broadcast(TotalScore);
}

void UScoreSubsystem::ResetRun()
{
    TotalScore = 0;
    OnScoreChanged.Broadcast(TotalScore);
}

Blueprint側:呼ぶ側と表示する側

加点する側は、敵の撃破処理から1本つなぐだけです。

BP_Enemy(撃破時)
Event Destroyed
  → Get Score Subsystem → Add Score(Amount: 100)

注意: Event Destroyed は撃破以外の理由(レベル遷移でのワールド破棄など)でも呼ばれます。「倒したときだけ加点」を厳密にしたいなら、撃破処理から専用のイベント(OnKilled など)を鳴らし、そこから加点するのが安全です。ここでは動きを見るために簡単な Event Destroyed で組んでいます。

表示側は、Tickで毎フレーム読むのではなく OnScoreChanged にBindします。スコアが変わった瞬間だけ更新されるので、Tickを減らす設計にもそのまま乗ります。

WBP_ScoreHUD(表示側)
Event Construct
  → Get Score Subsystem
  → Bind Event to On Score Changed
      Event ピン ← Custom Event: HandleScoreChanged(New Total Score を受け取る)
HandleScoreChanged → Set Text(Text: New Total Score を文字列化)

Event Destruct
  → Get Score Subsystem → Unbind Event from On Score Changed

Bindしたら必ずUnbindまで書きます。理由はEvent Dispatcherの記事にまとめています。

確認する

Level1 に敵を3体置き、すべて倒してから Open Level(Level Name: Level2) でレベルを移動します。

  • Level1で3体倒すと、HUDの表示が 100 → 200 → 300 と増えます
  • Open LevelでLevel2へ移動しても、HUDは 300のまま です。ここが確認したい一点です
  • Level2で1体倒すと 400 になります
  • Output Logには ScoreSubsystem initialized.Playの開始時に1回だけ 出ます。レベル移動では出ません
  • ResetRun を確認用に R キーから呼ぶと TotalScore は0に戻り、GetHighScore400のまま 残ります

移動後に0へ戻ってしまう場合は、加点先がSubsystemではなくGameModeやGameStateになっている可能性が高いです。ScoreSubsystem initialized. がレベル移動のたびに出力されているなら、UGameInstanceSubsystem ではなく UWorldSubsystem を継承しています。ログの読み方はデバッグの記事を参照してください。

ポイントは2つです。

  • 持ち越したいかどうかで基底クラスが決まる: UGameInstanceSubsystemUWorldSubsystem は1単語しか違いませんが、レベル遷移で残るか消えるかという結果はまったく違います。迷ったら「Playを止めるまで覚えていてほしいか」を自分に聞いてください
  • 値は公開せず、関数を通して変える: TotalScore をprivateにして AddScore だけを出すと、書き換えられる場所が1箇所に固定されます。どこからでも触れる置き場所ほど、入口を狭くしておく価値があります
Sponsored

おまけ:先に知っておくと良いこと

  • Subsystemの作成にはC++が必要: Blueprintだけでは作れません。C++を入れたくない段階なら、GameInstanceを継承したBlueprintで代用し、機能が増えてきたらSubsystemへ移す順番でも構いません
  • Tickは標準では回らない: Subsystemに毎フレームの処理が必要なら、UTickableWorldSubsystem を継承します。必要ないなら継承しない方が、無駄なTickが増えません(→ Tickを減らす設計
  • Subsystem同士を直接呼び合わない: 依存が絡むと初期化順の問題が戻ってきます。どうしても順序が必要なら、Initialize の中で Collection.InitializeDependency<UOtherSubsystem>() を呼ぶと、先に初期化されることを保証できます
  • セーブとは別の話: Subsystemが覚えているのはゲームを終了するまでです。次回の起動に残したい値は、セーブ・ロードの仕組みでディスクへ書き出します
  • World Subsystemは思ったより多く作られる: エディタのプレビュー用ワールドなどでも生成されることがあります。ゲーム中だけに限定したい場合は ShouldCreateSubsystem() で条件を書きます

まとめ

  • Subsystemは エンジンが自動で作って壊してくれる、機能ごとの置き場所 。GameInstanceを書き換えずに機能を足せる
  • 5種類の違いは 生存期間だけ 。ゲームプレイでは GameInstance / World / LocalPlayer の3つを見る
  • 最頻出は GameInstance Subsystem 。レベル遷移で壊されないので、持ち越したい値の置き場所になる
  • 自作シングルトンとの差は 初期化・破棄・PIEでの作り直しをエンジンが引き受ける こと
  • 作成はC++だが、 使うのはBlueprintだけで完結する (Get ◯◯ Subsystem ノード)
  • 実践の要は 持ち越したいなら GameInstance、値はprivateにして関数で変える

いまGameInstanceやGameModeに置いてある変数を眺めてみてください。レベルをまたいで残したいものと、そうでないものが混ざっていませんか。その境界が、そのままSubsystemの分け方になります。