ステージを移動してもスコアが残るよう、GameInstanceへ変数を作った。次に音量設定、セーブ、実績も足していくと、スコアを直したいだけなのに、ほかの機能まで並んだ大きなクラスを開くことになります。
Subsystem は、こうした機能を分けて置くための仕組みです。今回はスコアの処理を一つのクラスにまとめ、Blueprintから「100点を足す」「現在の点数を読む」と呼び出します。ステージを移動しても300点はそのまま。もう100点を足せば400点になります。
この記事でわかること
- 機能を分けるSubsystemと、その持ち主の関係
- GameInstance・World・LocalPlayerの選び方
- C++で作ったスコアの処理を、Blueprintから呼ぶ方法
- レベル移動、リセット、Playし直しで何が残るか
Subsystemは、機能ごとの置き場所
GameInstanceは、ゲームの実行中、レベルを移動しても残る置き場所です。まだスコアを一つ持たせたい段階なら、GameInstanceのBPを使う方法で十分に作れます。
Subsystemが役立つのは、そこへ置く機能が増えてきたときです。スコア、音量、セーブを別々のクラスに分ければ、スコアの変更ではスコアのクラスを開けば済みます。

GameInstance Subsystem は、GameInstanceを持ち主として動きます。自作のスコア用クラスを用意すると、UEがGameInstanceごとにその個体を作ってくれます。レベルへActorを置いたり、GameInstanceに生成用の処理を足したりする必要はありません。
「クラス」は仕組みを書いた設計図、「個体」は実行中に値を持つ実物です。後で使うGetノードは、設計図から毎回作り直すのではなく、そのGameInstanceが持っている同じ個体を取り出します。
誰の分を持ち、いつまで残るか
Subsystemを選ぶときは、二つのことを考えます。誰の分を別々に持つか と、いつまで覚えていてほしいか です。前者をスコープ、後者を生存期間と呼びます。
たとえば、画面分割で2人が遊ぶゲームなら、通算スコアを共有しても、入力設定は1Pと2Pで分けたいかもしれません。どちらもレベル移動後に残したい値ですが、同じ置き場所が適するとは限りません。

| 種類 | 持ち主と残る期間 | 使いどころ |
|---|---|---|
| GameInstance | GameInstanceごと。ゲームの実行を終えるまで | ステージをまたぐスコア、セーブ管理 |
| World | Worldごと。そのワールドが終了するまで | ワールド内の敵や出現場所の管理 |
| LocalPlayer | 手元のLocalPlayerごと。そのプレイヤーが取り除かれるまで | 1P・2Pそれぞれの入力、UI設定 |
| Engine | エンジンの稼働中 | 複数のワールドにまたがるエンジン側の機能 |
| Editor | エディタの稼働中 | アセットの整理な どの制作ツール |
LocalPlayerの「Local」は、この端末で操作するプレイヤー という意味です。オンラインで接続した相手全員に一つずつ作られる、という意味ではありません。
また、Engine Subsystemはゲームの実行時にも使われます。Editor Subsystemと一緒に「エディタ専用」とは考えないでください。ゲーム制作ではまず、上の3種類から目的に合うものを選ぶと整理しやすくなります。
Worldとレベルファイルは同じ単位ではない
World は、Actorが活動するゲーム世界のまとまりです。一つのWorldへ、複数のレベルやエリアを追加で読み込むこともあります。
今回使うOpen Levelでは、移動先のマップを開き、Worldを入れ替えます。この場合、World Subsystemも作り直されます。一方、同じWorldの一部を読み込むストリーミングでは、エリアが増えただけでWorld Subsystemが作り直されるわけではありません。

そのため「部屋を移るたびに敵の数を0へ戻したい」なら、World Subsystemへ置くだけでなく、部屋の切り替え時にリセットする処理も必要です。ゲーム内の区切りと、UEが作り直す区切りは分けて考えます。
自分で一つだけ作る方法との違い
プログラムで「共通の個体を一つだけ使う」設計を、シングル トン と呼びます。自作する場合は、いつ作り、いつ片付けるかも自分で決めます。
たとえば、個体ごとではなくプログラム側で共有する static変数 へスコアを置くと、エディタを開いている間、Stop後も値が残ることがあります。次のPlayで前回の300点から始まってしまうと、ゲームのリセット処理が正しいか分かりづらくなります。
GameInstance Subsystemなら、新しいPlayのGameInstanceに、新しいスコア用の個体が作られます。開始時に呼ばれる Initialize() へ初期値を書き、終了時の Deinitialize() へ片付けを書く、という形にそろえられます。

これはGameInstance Subsystemの場合です。Engine Subsystemまで、Playのたびに作り直されるわけではありません。また、GameInstanceへ直接書いた変数もPlayごとに新しくなるため、直接書く方法から移す主な利点は、機能を分けやすいこと にあります。
実践の準備:2つのマップでスコアを試す
ここからは、スコア用の ScoreSubsystem を作ります。C++への第一歩で使った CppFirstStep プロジェクトを前提にします。C++のビルド環境と、Blueprintでノードをつなぐ操作を準備しておいてください。
まずは敵やHUDを作る前に、キーを押して数字を変えます。処理が動いたかは、画面の文字とOutput Logで確かめます。

| キー | 動作 |
|---|---|
| P | 100点を足し、現在の点数を表示 |
| N | Level1からLevel2を開く |
| R | 今回の合計を0へ戻す。実行中の最高点は残す |
- 三人称視点で歩けるマップを「Save Current Level As」で
Level1として保存します。 - そのマップを複製して
Level2を作ります。どちらもPlayer Startを残します。 - マップを見分けられるよう、Level2だけ大きなCubeを追加します。
- Level1を開き直します。Playの設定は「Number of Players」を1、「Net Mode」を「Play Standalone」にします。実行先は通常の「Selected Viewport」で構いません。
以下の操作は、各マップの Level Blueprint へ作ります。レベルエディタの「Blueprints」メニューから「Open Level Blueprint」で開けます。レベルを切り替えても値が残るかを見るため、スコアそのものはここに変数として作りません。
C++でスコア用のSubsystemを作る
「Tools → New C++ Class」で「All Classes」を選び、GameInstanceSubsystem を検索します。このクラスを親にし、名前を ScoreSubsystem として作成してください。
生成された .h と .cpp を、下のコードへ置き換えます。CPPFIRSTSTEP_API はこのプロジェクトの名前に対応する部分です。別のプロジェクトなら、自動生成されたAPIマクロを残してください。
値を持つ部分と、外から呼べる入口
TotalScore は今回の合計、HighScore はゲームの実行中に出した最高点です。どちらも最初は0です。private に置き、Blueprintには直接書き換えさせず、加点とリセットの関数を通して変更します。

// ScoreSubsystem.h
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "ScoreSubsystem.generated.h"
UCLASS(BlueprintType)
class CPPFIRSTSTEP_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(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;
};
BlueprintCallable はBPから呼べる処理、BlueprintPure は値を読み取る入口です。後者のノードには白い実行ピンがなく、つないだ先で値が必要になったときに呼ばれます。
初期化・加点・リセットの中身
// ScoreSubsystem.cpp
#include "ScoreSubsystem.h"
void UScoreSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
TotalScore = 0;
HighScore = 0;
UE_LOG(LogTemp, Log, TEXT("ScoreSubsystem initialized. Total=0 High=0"));
}
void UScoreSubsystem::Deinitialize()
{
UE_LOG(LogTemp, Log, TEXT("ScoreSubsystem deinitialized."));
Super::Deinitialize();
}
void UScoreSubsystem::AddScore(int32 Amount)
{
if (Amount <= 0)
{
return;
}
TotalScore += Amount;
HighScore = FMath::Max(HighScore, TotalScore);
UE_LOG(LogTemp, Log, TEXT("Score +%d Total=%d High=%d"),
Amount, TotalScore, HighScore);
}
void UScoreSubsystem::ResetRun()
{
TotalScore = 0;
UE_LOG(LogTemp, Log, TEXT("Score reset. Total=%d High=%d"),
TotalScore, HighScore);
}
Amount は足す点数で、0以下は加点しません。FMath::Max は二つの値の大きい方を選ぶので、合計が過去の最高点を超えたときだけHighScoreが上がります。ResetRun() はTotalScoreだけを0にするため、最高点は残ります。
Initializeは、UEがSubsystemの利用開始時に呼ぶ初期化処理です。コンストラクタとは別の呼び出しで、今回は二つの点数を0にしています。Super:: は親クラスの処理を呼ぶ書き方です。Deinitializeでは、終了したことをログに残しています。
ビルドして、クラスを使えるようにする
コードとアセットを保存し、UEを閉じます。Visual Studioで「Development Editor」「Win64」を選び、ゲームの CppFirstStep プロジェクトをビルドします。成功したら .uproject を開き直してください。
今回C++で定義したクラスは自動生成の対象なので、Spawn ActorやConstruct Objectで作りません。「Maps & Modes」のGame Instance ClassをScoreSubsystemへ変える操作も不要です。
Blueprintで現在値を表示する
Level1のLevel Blueprintを開き、右クリックで Get Score Subsystem を検索します。このノードの青い出力は、実行中のScoreSubsystemへの 参照 です。「この個体へ処理を頼む」と指定するときに使います。
青い出力からドラッグして Get Total Score を作ると、その参照がTargetにつながります。Targetは「誰の合計を読むか」です。Getノードを複数置いても、同じGameInstance内では同じスコアを読みます。
表示する処理を一つ作る
右クリックからCustom Eventを追加し、ShowTotal と名付けます。引数は追加しません。このイベントに、現在の合計を表示する処理をまとめます。

- Get Score Subsystemの青い出力を、Get Total ScoreのTargetへつなぎます。
- Get Total Scoreの整数出力から
To String (Integer)を作ります。数字をPrint Stringで表示できる文字列へ変えるノードです。 - Print Stringを追加し、変換後の文字列を「In String」へつなぎます。
- ShowTotalの白い実行線をPrint Stringへつなぎ、Durationを
5.0にします。

図の A は、前の図からつながる文字列の目印です。ノードを追加する必要はありません。Print Stringの「Print to Screen」と「Print to Log」を有効にしておきます。
次に、Event BeginPlay から Show Totalを呼ぶノード をつなぎます。右クリックでShow Totalを検索するか、My Blueprintに作ったイベントをグラフへドラッグして呼び出します。Custom Eventの定義をもう一つ作る操作ではありません。

CompileしてPlayすると、画面に0が出ます。ここで0を表示できれば、Subsystemを取得し、C++の関数をBPから呼べています。
加点・移動・リセットをつなぐ
Pを押して100点を足す
Level BlueprintでPキーのイベントを追加します。白い実行線は、Pressed → Add Score → Show Total の順です。
Get Score Subsystemの参照をAdd ScoreのTargetへつなぎ、Amountを 100 にします。加点してから表示する順 なので、押した直後の点数が見えます。

Play画面をクリックしてからPを3回押し、100→200→300と増えるか確かめます。Released側はつなぎません。ここでは、1回のキー入力を1回の加点に対応させています。
Rで今回の合計を0へ戻す
Rキーの Pressed → Reset Run → Show Total をつなぎます。Reset RunのTargetもGet Score Subsystemです。Reset Runに引数はありません。
