セーブデータ、設定値、ステージをまたいで持ち越すスコア。どこからでも触りたいけれど、どのアクターに置いても収まりが悪いものがあります。とりあえずGameInstanceに変数を足していくと、半年後には何十個もの変数が並んだクラスができあがります。
UEには、こうした機能のための置き場所が用意されています。 Subsystem です。この記事では、5種類のSubsystemの生存期間の違いと、シングルトンやGameInstance直書きと比べた利点、Blueprintからの使い方、そしてレベルをまたぐスコア管理を組む手順を解説します。
この記事でわかること
- Subsystemの正体= エンジンが自動で作って壊してくれる、機能ごとの置き場所
- 5種類の生存期間 の違いと選び方
- GameInstance Subsystem が最もよく使う理由
- シングルトン自作・GameInstance直書きと比べた 具体的な利点
- Blueprintから使う手順(Getノードで取り出す)
- 実践: レベルをまたいで持ち 越すスコアを管理する
なぜ必要か:置き場所に困るものがある
ゲームを作っていると、どのアクターにも属さない機能が出てきます。
- 累計スコアや所持金のように、 レベルを移動しても消えてほしくない値
- 音量やキーコンフィグのように、 どの画面からも読み書きしたい設定
- セーブ・ロードのように、 1つだけあれば十分な仕組み
置き場所として真っ先に思いつくのがGameInstanceですが、ここに全部を足していくと、無関係な機能が1つのクラスに同居します。スコアの処理を直したいのに開くファイルには音量もセーブも入っている、という状態です。まずBPだけでGameInstanceに値を持たせる方法は Game Instanceでデータを持ち越す にまとめてあり、その置き場所が散らかってきたときの次の一手が、この記事のSubsystemです。

Subsystemは、この分割をエンジンの仕組みとして提供します。 GameInstanceを一行も書き換えずに、機能ごとの箱を後から足せる のが本質です。箱の生成と破棄はエンジンが行うため、こちらは中身だけを書きます。
5種類の生存期間
Subsystemは5種類あり、違いは どこにぶら下がり、いつからいつまで生きるか だけです。書き方はどれも同じなので、選ぶときはこの1点を見ます。

| 種類 | 継承する基底クラス | 生きている期間 | 向いているもの |
|---|---|---|---|
| GameInstance | UGameInstanceSubsystem | ゲーム起動から終了まで(レベル遷移を跨ぐ) | スコア、設定、セーブ管理、実績 |
| World | UWorldSubsystem | レベルのロードからアンロードまで | 敵のスポーン管理、そのレベル専用の進行 |
| LocalPlayer | ULocalPlayerSubsystem | プレイヤー追加から削除まで | プレイヤーごとのUI設定、入力の割り当て |
| Engine | UEngineSubsystem | エンジンの起動から終了まで | エンジン全体で1つの低レベルな仕組み |
| Editor | UEditorSubsystem | エディタの起動から終了まで | エディタ拡張、制作ツール |
ゲームプレイで使うのは、上の3つです。中でも GameInstance Subsystemが最もよく使われます 。レベルを移動しても壊されないという性質が、「持ち越したいもの」の要件とそのまま一致するためです。
判断はこの順で考えると迷いません。
- レベルを移動しても残ってほしいか → はいなら GameInstance
- そのレベルの中だけで完結するか → はいなら World
- プレイヤーごとに別々に持ちたいか → はいなら LocalPlayer
1本のローグライトで並べるとイメージしやすくなります。ラン中ずっと持ち越す所持金は GameInstance 、いま入っている階の敵スポーン管理は World 、画面分割の2P側だけに効くUI設定は LocalPlayer 。同じゲームの中でも、残る長さで置き場所が分かれます。
補足: Engine SubsystemとEditor Subsystemは、ゲームの中身ではなくエディタ側の道具を作るときに使います。ゲームプレイの機能を置くと、パッケージしたビルドで意図しない挙動になることがあるので、最初は上の3つだけを見ておけば十分です。
シングルトン自作と比べて何が良いのか
「どこからでも触れる1つだけの置き場所」は、自分でシングルトンを書いても実現できます。それでもSubsystemを使う理由は、面倒な部分をエンジンが引き受けてくれるからです。
| 気にすること | 自作シングルトン | Subsystem |
|---|---|---|
| いつ作るか | 自分で書く。初期化順のバグが出やすい | エンジンが Initialize() を呼ぶ |
| いつ壊すか | 自分で書く。消し忘れると値が次回起動に残る | エンジンが Deinitialize() を呼ぶ |
| PIE(エディタ実行)での状態 | 前回の実行の値が残ることがある | GameInstanceごとに作り直される |
| Blueprintから使う | 取得用の関数を自分 で用意する | 専用のGetノードが最初から出る |
| 依存の順序 | 手作業で調整 | Collection.InitializeDependency<T>() で指定できる |

特に効くのが 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);
}
Blueprintから使う
作成にC++が必要というだけで、 使う側はBlueprintだけで完結します 。ここを知らずにC++でしか触れないと思い込んでいる人が多い部分です。

イベントグラフで右クリックし、クラス名で検索します。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に置けば、この問題が消えます。
完成形

再現条件
UScoreSubsystem(親クラス:UGameInstanceSubsystem)に、次のメンバを用意します。
| メンバ名 | 型 | 初期値 | 指定子 |
|---|---|---|---|
TotalScore | int32 | 0 | private(BPには出さない) |
HighScore | int32 | 0 | private |
OnScoreChanged | FOnScoreChangedSignature(int32 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に戻り、GetHighScoreは 400のまま 残ります
移動後に0へ戻ってしまう場合は、加点先がSubsystemではなくGameModeやGameStateになっている可能性が高いです。ScoreSubsystem initialized. がレベル移動のたびに出力されているなら、UGameInstanceSubsystem ではなく UWorldSubsystem を継承しています。ログの読み方はデバッグの記事を参照してください。
ポイントは2つです。
- 持ち越したいかどうかで基底クラスが決まる:
UGameInstanceSubsystemとUWorldSubsystemは1単語しか違いませんが、レベル遷移で残るか消えるかという結果はまったく違います。迷ったら「Playを止めるまで覚えていてほしいか」を自分に聞いてください - 値は公開せず、関数を通して変える:
TotalScoreをprivateにしてAddScoreだけを出すと、書き換えられる場所が1箇所に固定されます。どこからでも触れる置き場所ほど、入口を狭くしておく価値があります
おまけ:先に知っておくと良いこと
- 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の分け方になります。