所持金を保存できるようになったゲームに、アップデートで「プレイ回数」も加えたい。これから遊ぶ人のデータは作れても、以前から遊んでいた人のセーブには、その項目がありません。
大切なのは、今までの所持金を残し、新しい項目にどんな値を入れるか決めること です。昔のデータを、今のゲームで使える形へ整える処理を 移行(マイグレーション) と呼びます。
この記事では、Save Game入門の所持金300を引き継ぎ、「更新後のプレイ回数」を追加します。保存形式の番号を見て必要なときだけ値を補い、次のロードでは補った値を上書きしないところまで確かめましょう。
この記事でわかること
- 古いデータにない項目と、既定値の関係を理解する
- SaveVersionで保存形式を区別する
- ロード直後に移行し、保存の成否を確かめる
- 2回目のロードや未対応の形式も試す
Save Game入門で作った、保存する値を持つ BP_DemoSave と、保存・ロードを呼ぶ BP_SaveDemo を使う発展編です。新しいプロジェクトで始める場合は、先に所持金の保存・ロードを作ってください。
古いセーブに、新しい項目は入っていない
以前のBP_DemoSaveには、所持金を保存するSavedGoldだけがありました。そこへPlayCountを追加しても、すでに保存したファイルへ過去のプレイ回数が書き足されるわけではありません。
既定値 は、新しい保存用の箱を作ったときに、各変数へ入る初めの値です。UEの標準の保存方式では、古いデータにない項目はクラスの既定値から始まり、削除された項目は読み込み時に無視されます。オブジェクトの読み書きに関する公式説明

ここで考えるのは、「エラーなく読めたか」だけではありません。その値を、ゲームでどう解釈するか です。
たとえばPlayCountが0でも、「一度も遊んでいない」のか、「昔は記録していなかった」のかは数字だけでは分かりません。昔の回数を保存していなければ、正確な通算回数は取り戻せません。
今回は、PlayCountを 「この更新以降に遊んだ回数」 と決めます。古い記録は移行時に0へそろえ、その後のプレイから数え始める方針です。旧データにないことを確認しやすいよう、保存用の型の既定値は-1にしておきます。
SaveVersionで「どの形式か」を見分ける
保存用の型に SaveVersion という整数を持たせます。「この記録は、どの項目・意味で保存されたか」を表す目印です。ゲームの製品バージョンや、UEのバージョンとは別に管理します。
今回の対応は次のとおりです。
| SaveVersion | 保存形式 | 読んだ後の扱い |
|---|---|---|
| 1 | 以前の形式。SavedGoldだけを保存していた | PlayCountを0にし、形式2へ移行する |
| 2 | 現在の形式。PlayCountも保存する | 保存された値をそのまま使う |
| それ以外 | この実装では扱い方を決めていない形式 | メッセージを出し、適用・上書き をしない |

Save Game入門のセーブにはSaveVersionもないため、この変数の既定値は1に固定 します。番号のない旧データを読んだとき、1として扱うためです。
現在の形式で新しく保存するときは、書き込む前に明示的に2を設定します。既定値を2へ変えて済ませると、番号のない古い記録まで形式2に見えてしまい、必要な移行を飛ばす原因になります。
最初からSaveVersionを持たせておけば、この「番号のない世代」の扱いを決める手間が減ります。ただし、番号だけでは値は変わりません。値を整える→番号を更新する→保存する までを一組にします。
準備:昔のセーブを残して、項目を足す
1. 項目を追加する前に、旧形式で保存する
Save Game入門で作ったBP_SaveDemoを開き、SlotNameの初期値を NotesMigrationDemo01 に変えます。移行の実験用に、別のスロットを使います。
まだBP_DemoSaveへ新項目を追加せず、Playして 1 で所持金300、2 で保存します。「セーブしました」を確認し、Playを停止してください。これが更新前の記録です。
PCでの実験なら、プロジェクトの Saved/SaveGames/NotesMigrationDemo01.sav を、テスト用のバックアップフォルダへコピーしておきます。移行後の保存で内容が更新されるため、元の記録 を残しておくと何度でも同じ条件で試せます。
2. 保存用の型とActorへ変数を追加する
BP_DemoSaveには、次の2変数を追加します。既存のSavedGoldの名前と型は変えません。
| 変数名 | 型 | 既定値 |
|---|---|---|
| SaveVersion | Integer | 1 |
| PlayCount | Integer | -1 |
次にBP_SaveDemoへ、次の2変数を追加します。
| 変数名 | 型 | 初期値 | 役割 |
|---|---|---|---|
| LoadedSave | BP_DemoSaveのObject Reference | None | 今ロードした保存用の箱を指す |
| PlayCount | Integer | 0 | ゲーム内で使う更新後のプレイ回数 |
同じPlayCountでも、BP_DemoSave側はファイルへ残す値、BP_SaveDemo側はゲーム中の値 です。保存・ロードで、両者の間をコピーします。SaveRefはSave Game入門と同じく、保存するときに作った箱の参照として使います。
両方をコンパイルして保存してください。ここから通常の保存処理を更新し終えるまでは、2 を押さずに作業します。
3. 所持金と回数を一緒に表示する
BP_SaveDemoのShowGoldで、Format Textの「Format」を次へ変えます。
所持金: {Gold} / 更新後の回数: {Plays}
「Gold」にはこれまでどおりGoldのGet、新しい「Plays」にはActor側のPlayCountのGetをつなぎます。ResultからPrint Textへの接続と、ShowGoldの白い実行線はそのままです。

Print TextはSave Game入門どおり画面・ログ出力を有効、Duration=10、Key=GoldStatusです。以降、別に置く結果メッセージ用のPrint Textも画面・ログを有効、Duration=10にし、こちらのKeyはNoneにします。
実装:読んで、整えて、保存してから使う
1. ゲームへ値を戻す処理をまとめる
まず「Add Custom Event」で ApplyLoadedSave を作ります。引数は追加しません。役割は、LoadedSaveから所持金と回数を読み、Actorへ戻して表示することです。
LoadedSaveのGetからドラッグして Get Saved Gold と Get Play Count を作ります。それぞれのTargetにLoadedSaveがつながった状態で、次のように接続してください。Getは値を読むだけのノードなので、白い実行線はつなぎません。図の「純粋」は、このように値だけを返すノードのことです。

| 保存用の箱から読む値 | Actor側の書き込み先 |
|---|---|
| Get Saved Gold | Set Goldの値 |
| Get Play Count | Set PlayCountの値 |
白い実行線は、ApplyLoadedSave→Set Gold→Set PlayCount→ShowGoldの順です。

このイベントは、ロードに成功し、形式も確認できた後に呼びます。BeginPlayからは呼ばず、開始時の表示は従来のShowGoldのままにしてください。開始直後のLoadedSaveは、まだ箱を指していないNoneだからです。
2. ロードした箱を持ち、形式ごとに分ける
Save Game入門で作った 3 キー→Load Game from Slot→Cast To BP_DemoSaveまでは使います。Cast Failedのメッセージも残します。
Cast成功後に直接Set Goldへ進んでいた白い線を外し、Set LoadedSave へつなぎます。「As BP Demo Save」をLoadedSaveの値へ渡してください。以前のロード後のSet GoldとShowGoldの呼び出しは、手順1のイベントへまとめたので、この経路では使いません。

Set LoadedSaveの後へ Switch on Int を置きます。これは、整数の値に応じて進む出口を選ぶノードです。ノードを選択し、「Start Index」を1、「Add Pin」で1と2の出口を作ります。「Default」も残してください。
LoadedSaveのGetから Get Save Version を作り、その整数出力をSwitchの「Selection」へ渡します。

出口の接続先は次のとおりです。
- 1:次の手順で作る移行処理へ
- 2:ApplyLoadedSaveの呼び出しへ
- Default:Print Textで
未対応のセーブ形式ですと出し、そこで止める

新しい版で作った形式3のセーブを、形式2までしか知らないゲームで開くこともあります。「2より小さくなければ全部使う」とせず、知っている形式だけを受け付ける 形にしています。Switchの公式説明
3. 形式1だけ、PlayCountと番号を更新する
LoadedSaveのGetから Set Play Count と Set Save Version を作ります。両方のTargetはLoadedSaveです。
Switchの1から、Set Play Count=0→Set Save Version=2の順に白い線をつなぎます。ここで変更する のは、ActorのPlayCountではなく、ロードした箱のPlayCount です。

SavedGoldは変更しません。以前の300をそのまま引き継ぎ、新しい回数だけを0から始めます。形式2の記録はこの経路を通らないので、すでに保存した回数を0へ戻しません。
4. 移行した箱を保存し、成功したら使う
Set Save Versionの後へ Save Game to Slot をつなぎます。入力は「Save Game Object」へLoadedSaveのGet、「Slot Name」へSlotNameのGet、「User Index」は0です。

白い出力をBranchへつなぎ、「Return Value」を「Condition」へ渡します。
True側はPrint Textで v1からv2へ移行して保存しました と出し、続けてApplyLoadedSaveを呼びます。False側は 移行結果を保存できませんでした と出して止めます。

成功側のPrint Textの白い出力から、ApplyLoadedSaveの呼び出しへつなぎます。下の図のPrint Textは、上の図の成功メッセージと同じノードです。

箱の値を変えることと、ファイルを更新することは別 です。保存しなければ、次のロードでもファイルには古い形式が残っています。今回は移行直後に保存して、書き込みが成功したことまで確認します。
なお、保存の失敗を検出できても、元のファイルが無傷だと保証されるわけではありません。実験前に残したコピーは、検証が終わるまで保持してください。
5. 通常のセーブも、形式2で書く
2 キーからの保存処理も更新します。Save Game入門で作ったSet Saved GoldとSave Game to Slotの間へ、次の2処理を追加します。
- SaveRefのGetからSet Play Countを作り、値にActor側のPlayCountのGetを渡す
- SaveRefのGetからSet Save Versionを作り、値を2にする
白い線はSet Saved Gold→Set Play Count→Set Save Version→元のSave Game to Slotです。2つのSetノードのTargetは、今回保存する箱のSaveRefです。

移行時に保存する箱はLoadedSave、通常の保存で新しく作る箱はSaveRefです。ここを取り違えると、今のプレイ状況を保存したつもりで、前回ロードした値を書いてしまいます。
確認:2回目のロードでも値が残るか
確認用に、回数を変える操作を先に用意します。数字キー 5 の「Pressed」から、Actor側のSet PlayCount=5→ShowGoldをつなぎます。これは保存値の確認用で、プレイ回数を自動で数える処理ではありません。

コンパイルしてPlayし、2 で保存する前に 3 を押します。旧形式を読めるか確かめる前に新しい保存で上書きしないよう、順番に気を付けてください。
v1からv2へ移行して保存しました と出て、所持金: 300 / 更新後の回数: 0 になれば、移行経路を通っています。続けて5、2の順に押して保存し、Playをやり直して読み戻します。確認の流れを表にすると、次のとおりです。
| 操作 | 期待する結果 |
|---|---|
| 旧形式のある状態で3を押す | 移 行のメッセージ。所持金300、回数0 |
| 5を押す | 回数5になる |
| 2を押す | 通常の保存が成功する |
| Playを止め、再度Playして3を押す | 所持金300、回数5。移行のメッセージは出ない |

2回目のロードで回数が0へ戻った場合は、通常保存のSaveVersion=2と、PlayCountを箱へ写す接続を確認します。移行した箱を保存するときのTargetやSlotNameも見直してください。
実際のゲームでは、1プレイを終えたときなどにActor側のPlayCountを1増やしてから保存します。どの瞬間を「1回」と数えるかはゲームのルールに合わせて決めます。
未対応の番号でも試す
Playを止め、SlotNameを別の名前 NotesMigrationUnknown へ変えます。通常保存で入れるSaveVersionを一時的に99にし、コンパイルしてPlayしてください。
1 で所持金300、2 で保存、4 で所持金900にしてから、3 で読みます。未対応のセーブ形式です が出て、所持金が900のままなら、未対応の記録をゲームへ適用せず止められています。もう一度 3 を押しても同じメッセージが出ることを確かめます。
確認後はPlayを止め、SaveVersionの代入を2、SlotNameを NotesMigrationDemo01 へ戻します。BP_DemoSaveの既定値1は変更しません。 セーブがない場合のCast Failedも、前の記事の未使用スロットでの確認方法を使えます。
旧形式からやり直す場合は、Playを止めてから実験用の .sav をバックアップした旧ファイルへ戻します。更新後の型で新しく保存したデータを、番号だけ1にしたものでは、「新項目が本当にない旧ファイル」を試したことになりません。
おまけ:名前・型・世代を増やすとき
変数の名前や型を変えるなら、旧項目を残す
項目の追加、削除、改名、型の変更は、それぞれ確認する点が違います。
| 変更 | 確認すること |
|---|---|
| 項目を追加 | 古い記録に対して、既定値がゲームの意味に合うか |
| 項目を削除 | その値を引き継ぐ必要がないか。後の移行処理が使っていないか |
| 名前を変更 | 古い名前の値を、新しい項目へ引き継げるか |
| 型や単位を変更 | 読めるかに加えて、値の範囲・小数・単位が合っているか |
型の組み合わせによってはUE側で互換変換されることもありますが、それだけでゲーム上の意味が保たれるとは限りません。たとえばミリ秒を秒へ変えたら、数値が読めても1000で割る処理が必要です。
Blueprintで見通しよく移行するなら、古い項目の名前と型を残し、新しい項目を別に追加して値を写す 方法が使えます。古いBestTimeMillisecondsを削除してからでは、移行元の値を読めません。

この例では整数同士の割り算で小数が切り捨てられないよう、Floatへ変換して 1000.0 で割ります。UEにはCore Redirectsによる名前の対応付けもありますが、導入時は実際の旧セーブを使って確認してください。
形式3へ進むときは、変換を順につなぐ
形式3を作る場合、形式1のデータには「1→2」と「2→3」の両方が必要です。形式2なら後者だけを通します。入口の番号を見て、必要な段階を順に進む と考えると整理できます。

それぞれの変換を関数へまとめ、対応する段階へ入る形にすると、ゲーム本編へ世代ごとの分岐を散らさずに済みます。形式3を読めるようにするなら、今回のSwitchにも3の出口と、2→3の移行経路を追加してください。
古い形式の利用者をサポートする間は、その形式からの移行処理とテスト用の旧セーブを残します。同じ形式を2回ロードした場合に、回数を0へ戻したり、特典をもう一度足したりしないかも確認します。
保存形式の移行と、ファイル破損への対策は別
SaveVersionは、読み取れた記録をどう扱うかの目印です。途中 で書き込みが止まったファイルを修復する機能ではありません。配布するゲームでは、別スロットへの書き込みや世代バックアップなど、元の記録を残す保存方法も検討します。
読み込みに失敗した場合も、すぐ初期値で同じスロットを上書きすると、復旧に使える記録を失うことがあります。再試行・バックアップからの復旧・新規開始を分けて扱います。
音量や解像度などの設定は、進行データとは別に残すと、セーブを消しても設定を保てます。設定画面の記事で、項目に応じた保存先を扱っています。保存項目を整理したくなったら、構造体も使えます。
まとめ
- 古いセーブに新しい項目は入っていない。読むと既定値になる
- SaveVersionで「どの形式か」を見分ける
- 移行したら、書き戻すまでが1組。書き戻さないと毎回移行が走る
- 2回目のロードでも値が残るかを必ず確かめる
項目を足すときの問いかけは、「古いセーブを読んだら、この値は何になるか」 です。答えられないなら、移行の処理が要ります。
保存そのものは Save Game、プレイ中だけの記憶は GameInstance で扱っています。