[UE5] Save Data Versioning: Keep the Old Gold and Add a New Field

Created: 2026-07-25Last updated: 2026-09-07

What happens to old saves when you add a field to UE's Save Game? Illustrates SaveVersion, defaults, migration, and re-saving through an example that carries gold over and adds a post-update play count, distinguishing old records from current ones and confirming values do not revert on a second load.

Your game can save gold, and an update should add a "play count". You can create data for new players, but existing players' saves have no such field.

What matters is keeping the gold they had and deciding what value goes into the new field . The logic that shapes old data into a form your current game can use is called migration .

This article carries over the 300 gold from the Save Game introduction and adds a "post-update play count". We look at a format number, fill in values only when needed, and confirm the next load does not overwrite what we filled in.

Carrying over 300 gold and starting the post-update play count from 0

What You'll Learn

  • The relationship between fields missing from old data and defaults
  • Distinguishing save formats with SaveVersion
  • Migrating right after loading and confirming the save succeeded
  • Testing a second load and an unsupported format

This builds on BP_DemoSave , which holds the saved values, and BP_SaveDemo , which calls save and load, from the Save Game introduction. Starting a new project means building gold saving and loading first.

Sponsored

Old saves do not contain the new field

The previous BP_DemoSave had only SavedGold. Adding PlayCount does not write a past play count into files already saved.

A default is the initial value each variable takes when a new save container is created. With UE's standard save format, fields missing from old data start from the class default and removed fields are ignored on load. Official documentation on object handling

The old file's SavedGold at 300 while the new PlayCount starts from the class default of -1

What we consider here is not just "did it read without an error". It is how the game interprets that value .

Even with PlayCount at 0, the number alone cannot say whether it means "never played" or "we did not record it back then". Without a saved past count, the exact lifetime total cannot be recovered.

Here we define PlayCount as "plays since this update" . Old records are set to 0 at migration and counting starts from subsequent plays. So it is easy to confirm the absence in old data, we set the saved type's default to -1.

SaveVersion tells you "which format this is"

Give the save type an integer called SaveVersion . It marks "which fields and meanings this record was saved with". It is managed separately from your game's product version and UE's version.

Our handling is as follows.

SaveVersionSave formatHandling after reading
1The old format, saving only SavedGoldSet PlayCount to 0 and migrate to format 2
2The current format, also saving PlayCountUse the saved values as they are
Anything elseA format this implementation does not defineShow a message; do not apply or overwrite
Three paths: migrating format 1, using format 2 as is, and halting on anything else

The Save Game introduction's saves have no SaveVersion either, so fix this variable's default at 1 . That treats numberless old data as 1.

When saving newly in the current format, explicitly set 2 before writing. Changing the default to 2 instead would make numberless old records look like format 2 and skip the migration they need.

Including SaveVersion from the start reduces the work of deciding how to treat that "numberless generation". The number alone does not change values, though. Fix up values → update the number → save is one set.

Preparation: keep an old save and add fields

1. Save in the old format before adding fields

Open BP_SaveDemo from the Save Game introduction and change SlotName's initial value to NotesMigrationDemo01 . We use a separate slot for the migration experiment.

Without adding new fields to BP_DemoSave yet, Play and press 1 for 300 gold and 2 to save. Confirm "Saved" and stop Play. That is the pre-update record.

Experimenting on PC, copy the project's Saved/SaveGames/NotesMigrationDemo01.sav into a test backup folder. Saving after migration updates the contents, so keeping the original lets you retry the same conditions repeatedly.

2. Add variables to the save type and the Actor

Add these two variables to BP_DemoSave. Do not change the existing SavedGold's name or type.

Variable nameTypeDefault
SaveVersionInteger1
PlayCountInteger-1

Next add these two to BP_SaveDemo.

Variable nameTypeInitial valueRole
LoadedSaveBP_DemoSave Object ReferenceNonePoints at the save container just loaded
PlayCountInteger0The post-update play count used in game

Despite the shared name, BP_DemoSave's is the value kept in the file and BP_SaveDemo's is the value during play . Saving and loading copy between them. SaveRef, as in the Save Game introduction, is the reference to the container created when saving.

Compile and save both. Until the normal save logic is updated below, work without pressing 2 .

3. Display gold and the count together

In BP_SaveDemo's ShowGold, change Format Text's "Format" to this.

Gold: {Gold} / Plays since update: {Plays}

Connect Gold's Get to "Gold" as before and the Actor-side PlayCount's Get to the new "Plays". The Result-to-Print Text connection and ShowGold's white exec line stay as they are.

Passing Gold and the Actor-side PlayCount into the same Format Text's Gold and Plays

Keep Print Text as in the Save Game introduction with screen and log output enabled, Duration=10, and Key=GoldStatus. From here, the separately placed result-message Print Texts also use screen and log output with Duration=10, but with Key=None.

Sponsored

Implementation: read, fix up, save, then use

1. Gather the logic returning values to the game

First create ApplyLoadedSave with "Add Custom Event". No arguments. Its job is reading gold and the count from LoadedSave, returning them to the Actor, and displaying.

Drag from LoadedSave's Get to create Get Saved Gold and Get Play Count . With LoadedSave connected to each Target, connect as follows. Gets only read values, so no white exec line runs through them. "Pure" in the diagram means nodes that only return values like this.

Reading LoadedSave's two fields with Gets and using each output to assign into the Actor
Value read from the save containerWhere it is written on the Actor
Get Saved GoldSet Gold's value
Get Play CountSet PlayCount's value

The white exec line runs ApplyLoadedSave → Set Gold → Set PlayCount → ShowGold.

Returning LoadedSave's SavedGold and PlayCount to the Actor and displaying with ShowGold

Call this event after loading succeeds and the format is confirmed. Do not call it from BeginPlay; keep the startup display as the existing ShowGold. LoadedSave right after start is None, pointing at no container yet.

2. Hold the loaded container and branch by format

Keep the 3 key → Load Game from Slot → Cast To BP_DemoSave from the Save Game introduction, along with the Cast Failed message.

Disconnect the white line that went straight from Cast success into Set Gold and connect it to Set LoadedSave . Pass "As BP Demo Save" into LoadedSave's value. The previous post-load Set Gold and ShowGold calls now live in the event from step 1 and are not used on this path.

Holding the loaded BP_DemoSave in LoadedSave on Cast success

Place Switch on Int after Set LoadedSave. That node selects an exit based on an integer. Select the node, set "Start Index" to 1, and use "Add Pin" to create exits 1 and 2. Keep "Default" too.

Create Get Save Version from LoadedSave's Get and pass its integer output into the Switch's "Selection".

Passing LoadedSave's SaveVersion into Switch on Int's Selection

The exits connect as follows.

  • 1 : to the migration logic built next
  • 2 : to the ApplyLoadedSave call
  • Default : Print Text Unsupported save format and stop there
The format 1 migration path, format 2 application, and unsupported-format halt shown as white exec lines

A format 3 save made by a newer build can be opened by a game that only knows up to format 2. Rather than "use everything not below 2", we accept only the formats we know . Official Switch documentation

3. Update PlayCount and the number for format 1 only

Create Set Play Count and Set Save Version from LoadedSave's Get. Both Targets are LoadedSave.

From the Switch's 1, wire Set Play Count=0 → Set Save Version=2. What changes here is the loaded container's PlayCount, not the Actor's.

Updating LoadedSave's PlayCount to 0 and SaveVersion to 2 only for format 1

We do not change SavedGold. The previous 300 carries over and only the new count starts from 0. Format 2 records never take this path, so an already-saved count is not reset to 0.

4. Save the migrated container and use it on success

Connect Save Game to Slot after Set Save Version. Pass LoadedSave's Get into "Save Game Object", SlotName's Get into "Slot Name", and 0 into "User Index".

Passing the migrated LoadedSave into Save Game Object

Connect its white output into a Branch and "Return Value" into "Condition".

True prints Migrated from v1 to v2 and saved and then calls ApplyLoadedSave. False prints Could not save the migration result and stops.

Splitting save success and failure messages on Save Game to Slot's Return Value

Connect the success Print Text's white output into the ApplyLoadedSave call. The Print Text in the diagram below is the same success-message node from the diagram above.

Calling ApplyLoadedSave only after the success message

Changing the container's values and updating the file are separate. Without saving, the next load still finds the old format in the file. Here we save right after migrating and confirm the write succeeded.

Note that detecting a failed save does not guarantee the original file is intact. Keep the copy you made before experimenting until verification is done.

5. Have normal saves write format 2 too

Update the 2 key's save logic. Between the Set Saved Gold and Save Game to Slot from the Save Game introduction, add these two.

  1. Create Set Play Count from SaveRef's Get with the Actor-side PlayCount's Get as its value
  2. Create Set Save Version from SaveRef's Get with the value 2

The white line runs Set Saved Gold → Set Play Count → Set Save Version → the original Save Game to Slot. Both Set nodes' Target is SaveRef, the container being saved this time.

Normal saving putting the current PlayCount and format number 2 into SaveRef before writing

The container saved during migration is LoadedSave and the container newly created by a normal save is SaveRef. Mixing them up writes the values loaded last time while you think you are saving current progress.

Sponsored

Confirm: do values persist on a second load?

Prepare an operation that changes the count for confirmation. From the number key 5 's "Pressed", wire the Actor-side Set PlayCount=5 → ShowGold. That is for confirming saved values, not logic counting plays automatically.

Setting the Actor-side PlayCount to 5 with the 5 key and displaying with ShowGold

Compile, Play, and press 3 before pressing 2 to save. Mind the order so you do not overwrite the old format with a new save before confirming it reads.

Migrated from v1 to v2 and saved appearing with Gold: 300 / Plays since update: 0 means you took the migration path. Then press 5 and 2 to save, restart Play, and read it back. As a table, the flow is as follows.

ActionExpected result
Press 3 with the old format presentThe migration message. Gold 300, count 0
Press 5The count becomes 5
Press 2The normal save succeeds
Stop Play, Play again, and press 3Gold 300, count 5. No migration message
Migrating to 300 and count 0 the first time, then keeping count 5 after saving it

If the count returns to 0 on the second load, check SaveVersion=2 in the normal save and the connection copying PlayCount into the container. Also revisit the Target and SlotName when saving the migrated container.

In a real game, you would increment the Actor-side PlayCount when a play ends and then save. Which moment counts as "one play" is decided by your game's rules.

Test an unsupported number too

Stop Play and change SlotName to a different name, NotesMigrationUnknown . Temporarily set the SaveVersion written on normal save to 99, compile, and Play.

Press 1 for 300 gold, 2 to save, 4 to make it 900 gold, then 3 to read. Unsupported save format appearing with gold staying at 900 means an unsupported record is stopped without being applied. Press 3 again and confirm the same message.

After checking, stop Play, return the SaveVersion assignment to 2, and SlotName to NotesMigrationDemo01 . Do not change BP_DemoSave's default of 1. The Cast Failed for a missing save can also be confirmed with the unused-slot method from the previous article.

To redo from the old format, stop Play and restore the experiment .sav from your backed-up old file. Data newly saved with the updated type and merely renumbered to 1 does not constitute testing "an old file that genuinely lacks the new field".

Bonus: renames, type changes, and more generations

To rename or retype a variable, keep the old field

Adding, removing, renaming, and retyping fields each have different things to confirm.

ChangeWhat to confirm
Adding a fieldWhether the default's meaning fits the game for old records
Removing a fieldWhether the value needs carrying over and whether later migration uses it
RenamingWhether the old name's value can be carried into the new field
Changing type or unitBeyond readability, whether ranges, decimals, and units line up

Some type combinations get converted by UE, but that alone does not preserve their meaning in the game. Changing milliseconds to seconds, for instance, reads fine numerically but needs a division by 1000.

To migrate readably in Blueprint, keep the old field's name and type and add the new field separately, copying values across . Deleting the old BestTimeMilliseconds first leaves nothing to read the source value from.

Keeping the old BestTimeMilliseconds at 12500 and dividing by 1000 into the new BestTimeSeconds of 12.5

In this example, we convert to Float and divide by 1000.0 so integer division does not truncate the decimals. UE also has name mapping via Core Redirects, but confirm with a real old save when adopting it.

Moving to format 3 chains the conversions

Building format 3 means format 1 data needs both "1 → 2" and "2 → 3". Format 2 takes only the latter. Look at the entry number and advance through the stages you need.

Format 1 going through both 1-to-2 and 2-to-3 while format 2 goes through 2-to-3 only

Gathering each conversion into a function and entering at the matching stage keeps generation branches from scattering through your game. To read format 3, add a 3 exit and a 2-to-3 migration path to our Switch.

While you support users on an old format, keep that format's migration logic and a test save from it. Also confirm that loading the same format twice does not reset the count or grant a bonus twice.

Format migration and corruption handling are separate

SaveVersion marks how to treat a record you could read. It is not a feature repairing a file whose write was interrupted. For a shipped game, consider save methods that preserve the original, such as writing to a separate slot or keeping generational backups.

When loading fails, immediately overwriting the same slot with initial values can lose a record useful for recovery. Handle retrying, restoring from backup, and starting fresh separately.

Keeping settings such as volume and resolution separate from progress data lets settings survive deleting a save. The settings screen article covers destinations suited to different items. To organize what you save, structs also help.

Summary

  • Old saves do not contain new fields; reading them yields defaults
  • SaveVersion tells you "which format this is"
  • After migrating, writing back completes the set . Without it, migration runs every time
  • Always confirm values persist on a second load

The question to ask when adding a field is "what does this value become when reading an old save?" If you cannot answer, you need migration logic.

Saving itself is covered in Save Game and in-session memory in GameInstance.

Unreal Engine Notes in this section98