ステージ1でコインを30枚集めて、次のステージへ。ところが、画面のカウントが0に戻ってしまった。プレイヤーの変数に入れたはずなのに、なぜ残らないのでしょうか。
Open Level で別のレベルを開くと、元のキャラクターや配置したActorは作り直されます。変数の値を残したいなら、そのActorより長く存在する置き場所が必要です。
そこで使うのが GameInstance です。今回のゲーム起動中、レベルが切り替わっても同じものが残ります。この記事では、3つのステージの得点を足し、最後の画面へ合計とクリア数を持ち越します。
他のエンジンでいうと: Unity で
DontDestroyOnLoadを付けた管理用オブジェクト、Godot の Autoload(シングルトン) に当たります。ただし「プロジェクトに1つだけ」ではなく、 ゲームの実行1つにつき1つ です。
この記事でわかること
- 自作のGameInstanceを作り、プロジェクトで使うよう登録する
- 別のActorから記録を更新し、次のレベルで読み出す
- 同じステージの二重加算を防ぐ
- 記録を残して再訪する場合と、新しい周 回を始める場合を分ける
Blueprintで変数・関数を作れる人向けです。Third Personテンプレートの1人プレイを使い、Open Level でレベルを丸ごと切り替える例として進めます。
GameInstanceは、今回のプレイ中の記憶
GameInstanceは、ゲームの開始時にUEが作り、ゲームを終了するまで使うオブジェクトです。Actorのようにレベルへ配置するものではなく、位置や見た目も持ちません。
今回のレベル切り替えでは、次の違いがあります。
| 置き場所 | 次のレベルを開いたとき |
|---|---|
| キャラクターや配置したActorの変数 | 元のActorが破棄され、新しいActorは初期値から始まる |
| GameModeやLevel Blueprintの変数 | 新しく開いたレベルのものに切り替わる |
| GameInstanceの変数 | 同じGameInstanceが残り、値もそのまま使える |

「キャラクターが残る」のではなく、「キャラクターの外へ預けた記録が残る」 と考えると、使いどころが分かりやすくなります。新しいレベルの表示側は、その記録を読み出します。
ただし、Playを停止してもう一度始めると、GameInstanceも新しくなります。製品版では、アプリを閉じるまでが一区切りです。タイトルへ戻るだけでは値は消えません。
GameInstanceだけが持ち越しの唯一の方法というわけではありませんが、Blueprintでレベルをまたぐ数値を扱う入口として使いやすい方法です。レベル遷移の記事も、この使い方につながります。
作って、プロジェクトへ登録する
1. GameInstanceを親にBlueprintを作る
コンテンツブラウザで右クリックし、「Blueprint Class」を選びます。親クラス選択の「All Classes」で GameInstance を検索し、BP_GameInstance を作ります。
開いて、次の2変数を追加します。コンパイルしてから初期値を確認してください。
| 変数名 | 型 | 初期値・用途 |
|---|---|---|
| TotalScore | Integer | 0。今回の合計点 |
| ClearedStages | NameのArray | 空。今回クリアしたステージのID一覧 |
Name は、Stage01 のような識別名を扱う型です。Array(配列) にすると、同じ型の値を一覧として持てます。ClearedStagesには、通過した順にStage01、Stage02、Stage03を記録します。
2. このクラスを使うように設定する
「Edit → Project Settings → Maps & Modes」を開き、「Game Instance Class」へ BP_GameInstance を指定します。

クラスを作るだけでは、まだ使われません。 ここで登録して初めて、UEが自作のBP_GameInstanceを作ってくれます。設定を変えたらPlayを停止し、もう一度開始してください。
スコアとクリア記録を更新する
GameInstanceを取得する入口は Get Game Instance です。これは、現在使われているGameInstanceへの 参照 を返します。参照とは「どのオブジェクトを使うか」を示すものです。呼ぶたびに新しく作る処理ではありません。
その出力を Cast To BP_GameInstance のObjectへ渡すと、自作の変数や関数を使えます。Castは、取り出した相手をBP_GameInstanceとして扱えるか確認する処理です。実際の接続は、後ほどゴールと表示用Actorで作ります。
先にBP_GameInstanceの中へ、「表示する」「クリアを記録する」の2つの関数を用意しましょう。
1. 現在の記録を表示するShowSummary
BP_GameInstanceの「Functions」に ShowSummary を作ります。入力・出力は追加せず、Pureは無効のままにします。今回は画面へ文字を出す処理なので、白い実行線で呼び出す関数にします。
Format Text を置き、Formatへ次を入力します。
合計: {Total} / クリア: {Cleared}
TotalへTotalScoreのGetをつなぎます。ClearedStagesのGetから Length を作り、その出力をClearedへつなぎます。Lengthは配列の項目数なので、3ステージを記録していれば3です。数値をTextへ変えるノードが自動で入る場合は、そのままで構いません。

関数の入口から Print Text へ白い実行線をつなぎ、In TextへFormat TextのResultを渡します。Print to ScreenとPrint to Logを有効、Duration=10、Key=RunSummaryにします。

これで、呼び出した時点の記録を画面とOutput Logへ出せます。同じKeyを使うので、画面上の表示は更新されます。
2. 初めてのクリアだけ加算するClearStage
同じBP_GameInstanceに関数 ClearStage を作り、入力を2つ追加します。
| 入力名 | 型 | 渡される値の例 |
|---|---|---|
| StageID | Name | Stage01 |
| Score | Integer | 120 |
今回は 「各ステージの得点は、1周につき1回だけ」 というルールにします。やり直して同じゴールを通っても、合計点は増やしません。
ClearedStagesのGetから Contains Item を作り、Item to Findへ入力のStageIDを渡します。Contains Itemは、「このIDが一覧にあるか」をtrue/falseで返します。
関数の入口からBranchへ白い線をつなぎ、ConditionへContains ItemのReturn Valueを渡します。True側はPrint Textで このステージは記録済みです と表示して終了。False側だけ、次の記録処理へ進めます。

Falseから、次の順で白い線をつなぎます。
Add Unique:Target ArrayへClearedStagesのGet、New Itemへ入力のStageIDSet TotalScore:値へ「TotalScoreのGet+入力のScore」を渡す。Integer同士の+を使うShowSummary:TargetはSelf

Add Uniqueは、同じIDを配列に重ねて入れない追加 方法です。これだけでは、その後のスコア加算は止まりません。 先にContains Itemで分岐することで、一覧と得点の両方を二重更新から守ります。Add UniqueのReturn Valueは今回は使いません。
記録済みメッセージなど、この記事のほかのPrint TextはDuration=5、Key=None、画面とログを両方有効にします。ここまでできたらBP_GameInstanceをコンパイルして保存し、ほかのBlueprintから関数を呼べる状態にします。
実践:3つのゴールから、記録して次へ進む
1. 4つのレベルを用意する
Third PersonのBlueprintプロジェクトで、「File → New Level → Basic」からレベルを作り、Content/Maps へ次の名前で保存します。
- L_Stage01
- L_Stage02
- L_Stage03
- L_Result
各レベルに歩ける床とPlayer Startを用意し、「World Settings」の「GameMode Override」を BP_ThirdPersonGameMode にします。Playでキャラクターが出現し、歩けることを確かめてください。テンプレートにVariant選択がある版では、Noneを使います。
2. ゴールActorを作る
Actorを親に BP_StageGoal を作ります。DefaultSceneRootの子へBox Collisionを追加し、Trigger と名付けます。Relative Locationは (0,0,100)、Box Extentは (100,100,100) にします。
TriggerのCollision PresetsをCustom、Collision EnabledをQuery Only、Object TypeをWorldDynamicにします。PawnだけOverlap、ほかはIgnoreにし、Generate Overlap Eventsを有効にします。
Overlap は、相手を壁のように押し返さず、領域へ入ったことを検知する設定です。BP_ThirdPersonCharacter側でもCapsule ComponentのGenerate Overlap Eventsを有効、Mesh側は無効にして、カプセルで検知します。
見える目印としてStatic Meshを追加し、Cubeを指定します。名前はMarker、Relative Location=(0,-120,60)、Scale=(0.15,0.15,1.2)、CollisionはNoCollisionにします。

BP_StageGoalに次の変数を作ります。
| 変数名 | 型 | 初期値 | Instance Editable |
|---|---|---|---|
| StageID | Name | None | 有効 |
| StageScore | Integer | 0 | 有効 |
| NextLevel | Name | None | 有効 |
| IsTransitioning | Boolean | false | 無効 |
Instance Editable を有効にすると、レベルへ置いた個体ごとに値を変えられます。同じBP_StageGoalを各ステージへ1つずつ置き、次を設定します。L_Resultにはゴールを置きません。
| 配置先 | StageID | StageScore | NextLevel |
|---|---|---|---|
| L_Stage01 | Stage01 | 120 | /Game/Maps/L_Stage02 |
| L_Stage02 | Stage02 | 80 | /Game/Maps/L_Stage03 |
| L_Stage03 | Stage03 | 150 | /Game/Maps/L_Result |
NextLevelは、次に開くレベルを示すNameです。ここでは名前の重複を避けるため、Content を /Game に置き換えたパスを使います。末尾の .umap は付けません。
ゴールはPlayer Startと重ならない場所に置きます。たとえば床の高さをZ=0としたとき、ゴールActorを (500,0,0)、Player Startを (0,0,100) にすれば、歩いて近づいて試せます。
3. プレイヤーが触れたときだけ反応する
BP_StageGoalでTriggerを選び、On Component Begin Overlapを追加します。白い出力をCast To BP_ThirdPersonCharacterへ、Other ActorをObjectへつなぎます。Other Actorは、いま領域へ入った相手です。
Cast成功からBranchへ進め、ConditionへIsTransitioningのGetを渡します。Trueはつながず終了、FalseからSet IsTransitioning=trueへつなぎます。

IsTransitioningは「このゴールは、すでに次へ進む処理を始めた」という札です。複数の接触が起きても、同じゴールから処理を重ねて始めないようにします。
4. GameInstanceへ記録してから、次を開く
Set IsTransitioning=trueの後を、Cast To BP_GameInstance の白い実行入力へつなぎます。Get Game Instance の青いReturn Valueは、CastのObjectへ渡します。

Get Game Instanceには白い実行ピンがありません。白い線は「いつ実行するか」、青い線は「どの相手を使うか」 を渡します。
Cast成功からClearStageの呼び出しへ進みます。Targetへ「As BP Game Instance」、StageIDへ自分のStageIDのGet、Scoreへ自分のStageScoreのGetを渡します。

ClearStageの白い出力から Open Level (by Name) へつなぎます。Level NameへNextLevelのGetを渡し、Absoluteはtrue、Optionsは空のままにします。

記録は、今のレベルを離れる前 に済ませます。次のレベルでは、すでに更新されたGameInstanceを読み出せます。
Cast To BP_GameInstanceの失敗側は、Set IsTransitioning=false→Print Textの順につなぎ、Game Instance Classを確認してください と表示します。失敗時にはOpen Levelへ進めません。キャラクターのCast失敗側は、対象外なので何もしません。
5. 新しいレベルで、今の記録を表示する
Actorを親に BP_RunStatus を作り、4つのレベルすべてに1つずつ置きます。見た目の部品は不要です。
BP_RunStatusではBeginPlay→Cast To BP_GameInstanceへ白い線をつなぎ、ObjectへGet Game Instanceの出力を渡します。成功からShowSummaryを呼び、Targetへ「As BP Game Instance」を渡します。失敗側は同じ設定確認メッセージを出します。

表示するたびにTotalScoreを0へ戻さない ことが大切です。BP_RunStatusは読み出す係で、記録を初期化する係ではありません。
確認:持ち越し・再訪・新しい周回
まず、3ステージを通す
すべて保存・コンパイルし、L_Stage01からPlayします。画面をクリックして操作し、ゴールの柱へ順に近づきます。
| 場面 | 表示される記録 |
|---|---|
| L_Stage01の開始 | 合計: 0 / クリア: 0 |
| Stage01をクリアしてL_Stage02へ | 合計: 120 / クリア: 1 |
| Stage02をクリアしてL_Stage03へ | 合計: 200 / クリア: 2 |
| Stage03をクリ アしてL_Resultへ | 合計: 350 / クリア: 3 |

遷移直前の文字を見逃しても、次のレベルのBeginPlayで再表示します。Output Logにも同じ記録が残ります。
記録を残して、もう一度訪れる
確認用にBP_RunStatusの「Class Defaults → Input → Auto Receive Input」をPlayer 0にします。イベントグラフへキーボードの R を追加し、PressedからOpen Level (by Name)へつなぎます。Level Nameには /Game/Maps/L_Stage01 を直接入れます。
もう一度L_Stage01から3ステージを通し、リザルトで R を押してください。最初のステージを開き直しても、記録は350/3のままです。再びゴールを通ると「記録済み」と表示され、次へ進んでも得点は増えません。
これで、GameInstanceが残ることと、同じStageIDの二重加算を防げることを一緒に確かめられます。
新しい周回は、記録を消してから始める
BP_GameInstanceへ関数 ResetRun を作ります。入口からSet TotalScore=0→Clear→ShowSummaryの順につなぎます。ClearはClearedStagesのGetから作った配列用ノードで、一覧を空にする処理です。
BP_RunStatusへキーボードの N を追加します。Pressed→Cast To BP_GameInstance、ObjectはGet Game Instance。成功からResetRunを呼び、Targetへ「As BP Game Instance」を渡します。その後でOpen Level (by Name)を呼び、/Game/Maps/L_Stage01 を開 きます。Cast失敗側には設定確認メッセージをつなぎます。

Play中に N を押すと0/0で始まり、もう一度3ステージを通せば350/3になります。レベルを開くことと、新しい周回にすることは別の処理 だと分かります。
最後にPlayを止めて再開すると、今度はGameInstance自体が新しくなり、初期値の0/0から始まります。RとNは確認用の操作なので、本番ではタイトル画面の「続ける」「はじめから」などへ置き換えられます。
うまく動かないとき
| 症状 | まず見るところ |
|---|---|
| 設定確認メッセージが出る | Game Instance ClassがBP_GameInstanceか。設定後にPlayを再開したか |
| ゴールに近づいても反応しない | TriggerとCapsuleのOverlap設定、Other Actor→Object、Cast後の白い線 |
| 次のレベルへ進まない | 配置したゴールのNextLevel、レベルの保存先、Open Levelの入力 |
| 次のレベルで0になる | BeginPlayで初期化していないか。レベル側の別変数を読んでいないか |
| 2つ目のゴールも記録済みになる | 個体ごとのStageIDがStage01/02/03になっているか |
| 再訪で得点だけ増える | Contains ItemのTrue側から加算へ進んでいないか |
| RやNが反応しない | BP_RunStatusを配置したか、Auto Receive InputがPlayer 0か、画面 へ入力フォーカスがあるか |
登録の意味も試すなら、Playを止めてGame Instance Classを既定のGameInstanceへ戻し、再生します。今度は設定確認メッセージが出るはずです。確認後はBP_GameInstanceへ戻してから、Playをやり直してください。
おまけ:初期化と、置くデータの選び方
InitとShutdownは、レベルの開始・終了とは違う
GameInstanceのイベントグラフではEvent InitとEvent Shutdownを使えます。それぞれ、GameInstanceが初期化されるときと、終了するときの入口です。
Init→Print Textで GameInstance: Init、Shutdown→Print Textで GameInstance: Shutdown をログへ出してみると、Play開始・停止と、レベル移動の違いを追えます。3ステージを通っても、Initはレベルごとに呼び直されません。
Initは、起動時に必要な値を準備する場所です。プレイヤーやUIがそろっている前提で触らず、画面やActorへの反映は、それぞれの準備ができた後に行います。また、強制終了などではShutdownが呼ばれる保証はないので、大切な保存を終了時だけに任せないようにします。
残すのは、次のレベルでも意味がある情報
今回残したのは、合計点とステージIDです。「L_Stage01にいたゴールActorそのもの」への参照は保存していません。

Actor参照は「いま存在するこの個体」を指します。GameInstanceから参照していても、レベルを離れて破棄されるActorが残るわけではありません。次のレベルで使う相手は、そのレベルで取得し直します。
| 持ち越したいこと | 記録の例 |
|---|---|
| 何点稼いだか | TotalScore=350 |
| どこをクリアしたか | Stage01、Stage02、Stage03 |
| 何を持っているか | アイテムIDと個数 |
| どこへ戻るか | レベル名とチェックポイントID |
参照をすべて禁止する必要はありません。制作時に用意したData Assetへの参照など、レベル上の一時的なActorとは性質が違うものもあります。参照先がいつまで存在するか を考えて選びます。IDも保存するだけでActorが自動復元されるわけではなく、IDに対応するものを探したり作ったりする処理が必要です。
次回の起動にも残すならSave Game
GameInstanceは「今回のプレイ中の記憶」、Save Gameへの保存は「次回へ渡す記録」 です。ゲーム中はGameInstanceで更新し、区切りで必要な値をSaveGameオブジェクトへコピーして、ファイルへ書き出します。次回はファイルから読んだ値をGameInstanceへ戻します。
SaveGameオブジェクトを作っただけではファイルに保存されません。Save Gameの記事で、書き込みと読み戻しを試せます。
機能が増えたら、役割ごとに分ける
スコア、音量、実績、保存の処理が一つのクラスへ集まり、どこを直すのか分かりにくくなったら、機能ごとの分割を考えます。GameInstanceとともに管理される GameInstance Subsystem が、その選択肢です。
最初から導入する必要はありません。まず今回のようにClearStageやResetRunへ変更の入口をまとめておくと、更新ルールを探しやすくなります。C++を使う分割の入口はSubsystemの記事で扱います。
パッケージ化して試す場合は、4つのレベルをビルド対象に含めます。名前で開けても、製品版にマップが入っていなければ移動できません。パッケージングの記事で確認してください。
まとめ
- GameInstanceは、今回のプレイ中ずっと残る記憶
- 作るだけでなく、Project Settingsへ登録して初めて使える
- 残すのは「次のレベルでも意味がある情報」。Actorへの参照は残さない
- いつ0へ戻すかは、周回の区切りに合わせて自分で決める
置き場所に迷ったときの問いかけは、「これは次のレベルでも意味があるか」 です。意味がないなら、そのレベルの中で持てば足ります。
次回の起動にも残すなら Save Game、機能ごとに分けたくなったら Subsystem へ進んでください。
参考:GameInstanceの公式API、Get Game Instance、Open Level (by Name)。