【Unreal Engine】Subsystem入門:スコアの処理を分けて、次のステージへ持ち越す

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

GameInstance・World・LocalPlayerの違いを、誰の分を持ち、いつまで残るかで整理。C++でスコア用のSubsystemを作り、Blueprintから加点・レベル移動・リセットを試します。

ステージを移動してもスコアが残るよう、GameInstanceへ変数を作った。次に音量設定、セーブ、実績も足していくと、スコアを直したいだけなのに、ほかの機能まで並んだ大きなクラスを開くことになります。

Subsystem は、こうした機能を分けて置くための仕組みです。今回はスコアの処理を一つのクラスにまとめ、Blueprintから「100点を足す」「現在の点数を読む」と呼び出します。ステージを移動しても300点はそのまま。もう100点を足せば400点になります。

同じスコアの箱を使いながら、プレイヤーがステージ1から2へ進むイメージ

この記事でわかること

  • 機能を分けるSubsystemと、その持ち主の関係
  • GameInstance・World・LocalPlayerの選び方
  • C++で作ったスコアの処理を、Blueprintから呼ぶ方法
  • レベル移動、リセット、Playし直しで何が残るか

Sponsored

Subsystemは、機能ごとの置き場所

GameInstanceは、ゲームの実行中、レベルを移動しても残る置き場所です。まだスコアを一つ持たせたい段階なら、GameInstanceのBPを使う方法で十分に作れます。

Subsystemが役立つのは、そこへ置く機能が増えてきたときです。スコア、音量、セーブを別々のクラスに分ければ、スコアの変更ではスコアのクラスを開けば済みます。

一つのGameInstanceへ集めていた処理を、スコア・音量・セーブ・実績のSubsystemに分ける

GameInstance Subsystem は、GameInstanceを持ち主として動きます。自作のスコア用クラスを用意すると、UEがGameInstanceごとにその個体を作ってくれます。レベルへActorを置いたり、GameInstanceに生成用の処理を足したりする必要はありません。

「クラス」は仕組みを書いた設計図、「個体」は実行中に値を持つ実物です。後で使うGetノードは、設計図から毎回作り直すのではなく、そのGameInstanceが持っている同じ個体を取り出します

誰の分を持ち、いつまで残るか

Subsystemを選ぶときは、二つのことを考えます。誰の分を別々に持つか と、いつまで覚えていてほしいか です。前者をスコープ、後者を生存期間と呼びます。

たとえば、画面分割で2人が遊ぶゲームなら、通算スコアを共有しても、入力設定は1Pと2Pで分けたいかもしれません。どちらもレベル移動後に残したい値ですが、同じ置き場所が適するとは限りません。

ゲーム全体で共有するスコア、ワールドの敵管理、1Pと2Pに分かれた入力設定
種類持ち主と残る期間使いどころ
GameInstanceGameInstanceごと。ゲームの実行を終えるまでステージをまたぐスコア、セーブ管理
WorldWorldごと。そのワールドが終了するまでワールド内の敵や出現場所の管理
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が作り直されるわけではありません。

Open LevelではワールドとそのSubsystemが入れ替わり、同じワールドへの追加読み込みでは同じSubsystemを使う

そのため「部屋を移るたびに敵の数を0へ戻したい」なら、World Subsystemへ置くだけでなく、部屋の切り替え時にリセットする処理も必要です。ゲーム内の区切りと、UEが作り直す区切りは分けて考えます。

自分で一つだけ作る方法との違い

プログラムで「共通の個体を一つだけ使う」設計を、シングルトン と呼びます。自作する場合は、いつ作り、いつ片付けるかも自分で決めます。

たとえば、個体ごとではなくプログラム側で共有する static変数 へスコアを置くと、エディタを開いている間、Stop後も値が残ることがあります。次のPlayで前回の300点から始まってしまうと、ゲームのリセット処理が正しいか分かりづらくなります。

GameInstance Subsystemなら、新しいPlayのGameInstanceに、新しいスコア用の個体が作られます。開始時に呼ばれる Initialize() へ初期値を書き、終了時の Deinitialize() へ片付けを書く、という形にそろえられます。

GameInstance SubsystemはPlay開始で初期化され、Open Levelでは残り、Stop後の次のPlayでは新しくなる

これはGameInstance Subsystemの場合です。Engine Subsystemまで、Playのたびに作り直されるわけではありません。また、GameInstanceへ直接書いた変数もPlayごとに新しくなるため、直接書く方法から移す主な利点は、機能を分けやすいこと にあります。

Sponsored

実践の準備:2つのマップでスコアを試す

ここからは、スコア用の ScoreSubsystem を作ります。C++への第一歩で使った CppFirstStep プロジェクトを前提にします。C++のビルド環境と、Blueprintでノードをつなぐ操作を準備しておいてください。

まずは敵やHUDを作る前に、キーを押して数字を変えます。処理が動いたかは、画面の文字とOutput Logで確かめます。

Level1でPを3回押すと300点、NでLevel2へ移動しても300点、もう一度Pで400点になる
キー動作
P100点を足し、現在の点数を表示
NLevel1からLevel2を開く
R今回の合計を0へ戻す。実行中の最高点は残す
  1. 三人称視点で歩けるマップを「Save Current Level As」で Level1 として保存します。
  2. そのマップを複製して Level2 を作ります。どちらもPlayer Startを残します。
  3. マップを見分けられるよう、Level2だけ大きなCubeを追加します。
  4. 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の内部にTotalScoreとHighScoreを持ち、外にはAddScore・ResetRun・2つの読み取り関数を公開する
// 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を読み、整数を文字列へ変換する
  1. Get Score Subsystemの青い出力を、Get Total ScoreのTargetへつなぎます。
  2. Get Total Scoreの整数出力から To String (Integer) を作ります。数字をPrint Stringで表示できる文字列へ変えるノードです。
  3. Print Stringを追加し、変換後の文字列を「In String」へつなぎます。
  4. ShowTotalの白い実行線をPrint Stringへつなぎ、Durationを 5.0 にします。
ShowTotalからPrint Stringを実行し、読み取った合計を5秒間表示する

図の A は、前の図からつながる文字列の目印です。ノードを追加する必要はありません。Print Stringの「Print to Screen」と「Print to Log」を有効にしておきます。

次に、Event BeginPlay から Show Totalを呼ぶノード をつなぎます。右クリックでShow Totalを検索するか、My Blueprintに作ったイベントをグラフへドラッグして呼び出します。Custom Eventの定義をもう一つ作る操作ではありません。

Event BeginPlayからShow Totalを呼び、新しく開いたレベルでも現在の合計を表示する

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 にします。加点してから表示する順 なので、押した直後の点数が見えます。

PのPressedでAdd Scoreを呼び、100点を足した後にShow Totalで表示する

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に引数はありません。

RのPressedでReset Runを呼び、0へ戻っ�た合計をShow Totalで表示する

合計は0へ戻りますが、HighScoreは残ります。Output Logの Score reset. Total=0 High=300 で確認できます。

NでLevel2へ移動する

Level1のLevel Blueprintに、Nキーの Pressed → Open Level (by Name) を作ります。Level Nameを Level2 にします。

NのPressedからOpen Level by Nameを呼び、Level2を開く

Compileして保存したら、Level2を開きます。こちらのLevel Blueprintにも、次を作ってください。

  • ShowTotalの定義と、現在値を読んで表示する処理
  • Event BeginPlay → Show Total
  • P → Add Score → Show Total
  • R → Reset Run → Show Total

同じノード群をコピーして構いません。Level2ではNによる移動は作りません。どちらのBeginPlayにもReset Runを置かない のが要点です。レベルを開くたびにリセットすると、Subsystemが残っていても値を0へ戻してしまいます。

動かして、持ち越しを確かめる

両方のマップを保存し、Level1へ戻ってからPlayします。途中でRを試した場合も、ここでは新しいPlayで0から始めます。

操作見える結果
Level1でPlay0。初期化ログが1回出る
Pを3回100→200→300
Nを押す大きなCubeのあるLevel2へ移動し、300が表示される
Level2でPを1回400
Rを押す0。ログは Total=0 High=400
Pを1回100。ログは Total=100 High=400
Stopして、もう一度Play合計も最高点も0。初期化ログが新しく出る

Open Levelの前後で ScoreSubsystem initialized. が増えず、移動先で300が表示されれば、値を持つ個体は残り、表示するレベル側だけが新しくなった ことが分かります。

Level1で300を貯めてLevel2へ移っても300のまま。初期化のログは1回しか出ない

画面の数字は5秒で消えます。表示が消えることと、Subsystemの値が消えることは別です。読み返すときはOutput Logを使ってください。

困ったこと確認する場所
Get Score Subsystemが見つからないC++ビルドの成功、BlueprintTypeの指定、親がGameInstanceSubsystemか
Pを押しても増えないPlay画面のフォーカス、Pressedの線、Add ScoreのTarget、Amount=100
表示が一つ前の点数になるAdd Scoreの後にShow Totalを呼んでいるか
Level2で数字が出ないLevel2にもShowTotalとBeginPlayからの呼び出しを作ったか
Level2で0になるBeginPlayでReset Runを呼んでいないか、別の変数を表示していないか
Level2が開かないマップの保存、Level Nameのつづり、Output Logのエラー
Sponsored

おまけ:ゲームへ組み込むとき

加点するきっかけを、ゲームの出来事へ変える

今回のPキーの代わりに、敵の撃破を確定した箇所からAdd Scoreを呼べば、撃破スコアとして使えます。同じ敵の死亡処理が繰り返されないよう、HPと倒れ方の役割分担で扱った死亡済みの判定も合わせます。

Destroy Actorは、敵を倒した以外の目的でも呼べる処理です。「消したら加点」とまとめず、ゲーム上の成功を決めたところから加点すると、不要になった敵を片付けただけで点が増えることを防げます。

本物のHUDへ広げるなら、Event Dispatcherのような変更通知を使う方法もあります。その場合も、表示を作った直後には現在値を読む ようにします。次の加点通知を待つだけだと、移動先の画面に以前の300点がまだ表示されません。

持ち越しと、保存と、通信は別

HighScoreも、今動いているゲームの中だけの値です。次の起動へ残すならSave Gameで保存します。

また、GameInstance Subsystemの値が、ネットワーク上の相手へ自動で共有されるわけではありません。マルチプレイの共有スコアは、GameStateなどの役割と通信を合わせて設計します。

初期化の時点で、レベルのActorがいるとは限らない

GameInstance SubsystemのInitializeは、ゲーム側の準備が早い段階で呼ばれます。ここでは値の初期化などを行い、まだ生成されていないプレイヤーや敵を探す処理を混ぜないようにします。

World向けには UWorldSubsystem、毎フレームの処理も必要なら UTickableWorldSubsystem があります。ただし、Tickを使いたいだけで、レベル移動後も残すべきデータをWorld側へ移すと、寿命まで変わります。まず持ち主を決め、更新方法を考えます。

同じ種類のSubsystemに初期化の依存がある場合は、Initialize内の Collection.InitializeDependency<T>() で先に準備する相手を指定できます。異なる持ち主のSubsystemまで、何でも先に作れる指定ではありません。

World Subsystemをゲーム用のWorldに限定する場合は、DoesSupportWorldType() などで対象を絞ります。エディタにもWorldがあるため、「ワールドができた=ゲームを開始した」とは限りません。

まずは、いま大きくなっているGameInstanceから一つの機能を選んでみてください。「どの持ち主に置くか」「外から何を頼めるか」が決まれば、スコア用の例と同じ形で分けていけます。

まとめ

  • Subsystemは「持ち主と同じ寿命で動く、機能ごとの入れ物」
  • GameInstanceはゲーム起動中、Worldはレベル中、LocalPlayerはそのプレイヤーの間
  • C++で作った処理も、印を付ければBlueprintから呼べる

選ぶときの問いかけは、「この機能は、いつからいつまで生きていればよいか」 です。それが持ち主を決めます。

まず1つの入れ物で試すなら GameInstance入門、C++の書き方は C++への最初の一歩 へ。

参考:SubsystemのプログラミングSubsystemの5種類World SubsystemLocalPlayer Subsystem

Unreal Engine このセクションのノート98