The common belief: UPROPERTY(SaveGame) controls what UGameplayStatics::SaveGameToSlot writes. It doesn’t. (Checked in GameplayStatics.cpp + Property.cpp.)

What actually happens:

  • SaveGameToSlot → SaveGameToMemory serializes the whole USaveGame through a proxy archive, and never turns on the SaveGame flag on that archive (ArIsSaveGame stays false).
  • FProperty::ShouldSerializeValue only skips non-SaveGame properties when the archive is in SaveGame mode. It isn’t here, so the flag is ignored → every non-Transient property gets saved, marked SaveGame or not.
  • The writer is persistent, so the properties it does skip are the Transient ones.

So:

  • The SaveGame flag only matters when you serialize with an archive where ArIsSaveGame = true. It does nothing on the plain SaveGameToSlot path.
  • On a USaveGame, leaving SaveGame off does not mean “won’t be saved.”

Making the SaveGame flag actually work

Serialize the object yourself and flip the flag on the archive. Now only UPROPERTY(SaveGame) fields are written:

TArray<uint8> Bytes;
FMemoryWriter Writer(Bytes, /*bIsPersistent*/ true);
FObjectAndNameAsStringProxyArchive Ar(Writer, /*bLoadIfFindFails*/ false);
Ar.ArIsSaveGame = true;      // THIS is what makes the SaveGame flag filter
Object->Serialize(Ar);       // only SaveGame-marked properties end up in Bytes

Loading mirrors it: FMemoryReader + FObjectAndNameAsStringProxyArchive(Reader, true) + Ar.ArIsSaveGame = true, then Object->Serialize(Ar). This is the pattern used to save/restore actor & component state into a byte blob.

⚠️ Gotcha: on the plain SaveGameToSlot path, delegates, runtime UObject pointers, and caches on your save object get serialized too. On load they come back — so a delegate you bound before is restored with its old binding, and binding again reads as a double-add (“delegate already bound”). Dangling object refs are the same story.

✅ Rule: mark anything runtime-only on a save object as Transient. Don’t trust “no SaveGame = not saved” — that’s not how SaveGameToSlot works.

TL;DR

  • SaveGameToSlot saves everything except Transient, and ignores the SaveGame flag.
  • The SaveGame flag only works with your own archive where ArIsSaveGame = true.
  • Put runtime state (delegates, live refs, caches) behind Transient.