Loading...
Loading...
Compare original and translation side by side
USaveGameFArchiveUSaveGameFArchive.agents/ue-project-context.mdULocalPlayerSaveGame/ue-project-context.agents/ue-project-context.mdULocalPlayerSaveGame/ue-project-contextUSaveGameUObjectGameFramework/SaveGame.hUPROPERTY(SaveGame)UGameplayStatics// MyGameSaveGame.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "MyGameSaveGame.generated.h"
USTRUCT(BlueprintType)
struct FInventoryItemData
{
GENERATED_BODY() // Required — missing GENERATED_BODY() breaks struct serialization silently
UPROPERTY(SaveGame) FName ItemID;
UPROPERTY(SaveGame) int32 Quantity = 0;
UPROPERTY(SaveGame) bool bIsEquipped = false;
};
UCLASS(BlueprintType)
class MYGAME_API UMyGameSaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY(SaveGame) int32 SaveVersion = 0; // Always include a version field
UPROPERTY(SaveGame) float PlayerHealth = 100.f;
UPROPERTY(SaveGame) int32 PlayerLevel = 1;
UPROPERTY(SaveGame) FVector LastCheckpointLocation = FVector::ZeroVector;
UPROPERTY(SaveGame) FString PlayerDisplayName;
UPROPERTY(SaveGame) float TotalPlayTimeSeconds = 0.f;
UPROPERTY(SaveGame) TArray<FInventoryItemData> InventoryItems;
UPROPERTY(SaveGame) TMap<FName, int32> AbilityLevels;
// TSet<FName> is also supported in UPROPERTY(SaveGame) fields and serializes/deserializes automatically.
// Asset references: FSoftObjectPath stores a string path — safe across saves
// Never use raw UObject* or hard TObjectPtr<> to content assets in save data
UPROPERTY(SaveGame) FSoftObjectPath LastEquippedWeaponPath;
};USaveGameGameFramework/SaveGame.hUObjectUPROPERTY(SaveGame)UGameplayStatics// MyGameSaveGame.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "MyGameSaveGame.generated.h"
USTRUCT(BlueprintType)
struct FInventoryItemData
{
GENERATED_BODY() // 必填项 — 缺少GENERATED_BODY()会导致结构体序列化静默失败
UPROPERTY(SaveGame) FName ItemID;
UPROPERTY(SaveGame) int32 Quantity = 0;
UPROPERTY(SaveGame) bool bIsEquipped = false;
};
UCLASS(BlueprintType)
class MYGAME_API UMyGameSaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY(SaveGame) int32 SaveVersion = 0; // 始终包含版本字段
UPROPERTY(SaveGame) float PlayerHealth = 100.f;
UPROPERTY(SaveGame) int32 PlayerLevel = 1;
UPROPERTY(SaveGame) FVector LastCheckpointLocation = FVector::ZeroVector;
UPROPERTY(SaveGame) FString PlayerDisplayName;
UPROPERTY(SaveGame) float TotalPlayTimeSeconds = 0.f;
UPROPERTY(SaveGame) TArray<FInventoryItemData> InventoryItems;
UPROPERTY(SaveGame) TMap<FName, int32> AbilityLevels;
// TSet<FName>同样支持在UPROPERTY(SaveGame)字段中使用,可自动序列化/反序列化。
// 资源引用:FSoftObjectPath存储字符串路径 — 跨存档安全
// 切勿在存档数据中使用原始UObject*或硬TObjectPtr<>指向内容资源
UPROPERTY(SaveGame) FSoftObjectPath LastEquippedWeaponPath;
};#include "Kismet/GameplayStatics.h"
static const FString SlotName = TEXT("MainSave");
static constexpr int32 UserIdx = 0; // Always 0 on PC; use GetPlatformUserIndex() on console
// Create the object first, populate its fields, then save
UMySaveGame* SaveGame = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
SaveGame->PlayerHealth = 75.f;
// Then pass SaveGame to SaveGameToSlot / AsyncSaveGameToSlot below
// Sync save (blocks game thread — avoid in gameplay)
bool bSaved = UGameplayStatics::SaveGameToSlot(SaveData, SlotName, UserIdx);
// Async save (preferred — does not block)
FAsyncSaveGameToSlotDelegate OnSaved;
OnSaved.BindUObject(this, &USaveManager::OnAsyncSaveComplete);
UGameplayStatics::AsyncSaveGameToSlot(SaveData, SlotName, UserIdx, OnSaved);
// Load
if (UGameplayStatics::DoesSaveGameExist(SlotName, UserIdx))
{
UMyGameSaveGame* Save = Cast<UMyGameSaveGame>(
UGameplayStatics::LoadGameFromSlot(SlotName, UserIdx));
}
// Async load
FAsyncLoadGameFromSlotDelegate OnLoaded;
OnLoaded.BindUObject(this, &USaveManager::OnAsyncLoadComplete);
UGameplayStatics::AsyncLoadGameFromSlot(SlotName, UserIdx, OnLoaded);
// Delete
UGameplayStatics::DeleteGameInSlot(SlotName, UserIdx);#include "Kismet/GameplayStatics.h"
static const FString SlotName = TEXT("MainSave");
static constexpr int32 UserIdx = 0; // PC平台始终为0;主机平台使用GetPlatformUserIndex()
// 先创建对象,填充字段,再执行保存
UMySaveGame* SaveGame = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
SaveGame->PlayerHealth = 75.f;
// 之后将SaveGame传入下方的SaveGameToSlot / AsyncSaveGameToSlot
// 同步保存(阻塞游戏线程 — 避免在游戏过程中使用)
bool bSaved = UGameplayStatics::SaveGameToSlot(SaveData, SlotName, UserIdx);
// 异步保存(推荐 — 不会阻塞线程)
FAsyncSaveGameToSlotDelegate OnSaved;
OnSaved.BindUObject(this, &USaveManager::OnAsyncSaveComplete);
UGameplayStatics::AsyncSaveGameToSlot(SaveData, SlotName, UserIdx, OnSaved);
// 加载
if (UGameplayStatics::DoesSaveGameExist(SlotName, UserIdx))
{
UMyGameSaveGame* Save = Cast<UMyGameSaveGame>(
UGameplayStatics::LoadGameFromSlot(SlotName, UserIdx));
}
// 异步加载
FAsyncLoadGameFromSlotDelegate OnLoaded;
OnLoaded.BindUObject(this, &USaveManager::OnAsyncLoadComplete);
UGameplayStatics::AsyncLoadGameFromSlot(SlotName, UserIdx, OnLoaded);
// 删除存档
UGameplayStatics::DeleteGameInSlot(SlotName, UserIdx);ULocalPlayerSaveGameGetLatestDataVersion()HandlePostLoad()UCLASS()
class MYGAME_API UMyLocalPlayerSave : public ULocalPlayerSaveGame
{
GENERATED_BODY()
public:
virtual int32 GetLatestDataVersion() const override { return 3; }
virtual void HandlePostLoad() override;
UPROPERTY(SaveGame) TMap<FName, int32> UnlockedAbilities;
};
void UMyLocalPlayerSave::HandlePostLoad()
{
Super::HandlePostLoad();
const int32 Ver = GetSavedDataVersion(); // version when last saved
if (Ver < 2) { UnlockedAbilities.Add(TEXT("Dash"), 1); }
// Ver < 3 migrations go here
}// Load or create (sync)
UMyLocalPlayerSave* Save = ULocalPlayerSaveGame::LoadOrCreateSaveGameForLocalPlayer(
UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot0"));
// Load or create (async)
ULocalPlayerSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer(
UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot0"),
FOnLocalPlayerSaveGameLoadedNative::CreateUObject(this, &AMyPC::OnSaveLoaded));
// Save back
Save->AsyncSaveGameToSlotForLocalPlayer(); // async (preferred)
Save->SaveGameToSlotForLocalPlayer(); // syncULocalPlayerSaveGameGetLatestDataVersion()HandlePostLoad()UCLASS()
class MYGAME_API UMyLocalPlayerSave : public ULocalPlayerSaveGame
{
GENERATED_BODY()
public:
virtual int32 GetLatestDataVersion() const override { return 3; }
virtual void HandlePostLoad() override;
UPROPERTY(SaveGame) TMap<FName, int32> UnlockedAbilities;
};
void UMyLocalPlayerSave::HandlePostLoad()
{
Super::HandlePostLoad();
const int32 Ver = GetSavedDataVersion(); // 上次存档时的版本
if (Ver < 2) { UnlockedAbilities.Add(TEXT("Dash"), 1); }
// Ver < 3的数据迁移逻辑写在此处
}// 加载或创建(同步)
UMyLocalPlayerSave* Save = ULocalPlayerSaveGame::LoadOrCreateSaveGameForLocalPlayer(
UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot0"));
// 加载或创建(异步)
ULocalPlayerSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer(
UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot0"),
FOnLocalPlayerSaveGameLoadedNative::CreateUObject(this, &AMyPC::OnSaveLoaded));
// 保存存档
Save->AsyncSaveGameToSlotForLocalPlayer(); // 异步(推荐)
Save->SaveGameToSlotForLocalPlayer(); // 同步FArchiveSerialization/Archive.hAr.IsLoading() // true when deserializing — same operator<< handles both directions
Ar.IsSaving() // true when serializing to output
Ar.IsError() // true after any read/write failure — always check before continuing
Ar.Tell() // current position (int64); -1 if not seekable
Ar.CustomVer(Key) // returns the registered version number for a FGuid keyFArchiveSerialization/Archive.hAr.IsLoading() // 反序列化时为true — 同一个operator<<可处理序列化和反序列化
Ar.IsSaving() // 序列化到输出时为true
Ar.IsError() // 读写失败后为true — 后续操作前务必检查
Ar.Tell() // 当前位置(int64);不可寻址时返回-1
Ar.CustomVer(Key) // 返回FGuid键对应的已注册版本号FMemoryWriterFMemoryReaderSerialization/MemoryWriter.hMemoryReader.hTArray<uint8>// Serialize to bytes
TArray<uint8> OutBytes;
FMemoryWriter Writer(OutBytes, /*bIsPersistent=*/true);
int32 Version = 2;
Writer << Version; // Serialize version header first — always
Writer << SomeData;
checkf(!Writer.IsError(), TEXT("Serialization failed"));
// Deserialize from bytes
FMemoryReader Reader(OutBytes, /*bIsPersistent=*/true);
int32 LoadedVersion = 0;
Reader << LoadedVersion;
if (LoadedVersion < 1 || Reader.IsError()) { /* corrupt data */ return; }
Reader << SomeData;FMemoryWriterFMemoryReaderSerialization/MemoryWriter.hMemoryReader.hTArray<uint8>// 序列化为字节数组
TArray<uint8> OutBytes;
FMemoryWriter Writer(OutBytes, /*bIsPersistent=*/true);
int32 Version = 2;
Writer << Version; // 始终先序列化版本头
Writer << SomeData;
checkf(!Writer.IsError(), TEXT("序列化失败"));
// 从字节数组反序列化
FMemoryReader Reader(OutBytes, /*bIsPersistent=*/true);
int32 LoadedVersion = 0;
Reader << LoadedVersion;
if (LoadedVersion < 1 || Reader.IsError()) { /* 数据损坏 */ return; }
Reader << SomeData;FBufferArchiveSerialization/BufferArchive.hFMemoryWriterTArray<uint8>FBufferArchive Buffer(/*bIsPersistent=*/true);
int32 Magic = 0x53415645; // 'SAVE'
Buffer << Magic;
Buffer << MyStruct; // requires operator<< overload
TArray<uint8> Bytes = MoveTemp(Buffer); // FBufferArchive IS a TArray<uint8>FBufferArchiveSerialization/BufferArchive.hFMemoryWriterTArray<uint8>FBufferArchive Buffer(/*bIsPersistent=*/true);
int32 Magic = 0x53415645; // 'SAVE'
Buffer << Magic;
Buffer << MyStruct; // 需要重载operator<<
TArray<uint8> Bytes = MoveTemp(Buffer); // FBufferArchive本质是TArray<uint8>operator<<FArchiveFBufferArchiveFMemoryWriterFArchive& operator<<(FArchive& Ar, FMyCustomData& Data)
{
Ar << Data.Name << Data.Value << Data.Timestamp;
return Ar;
}operator<<FArchiveFBufferArchiveFMemoryWriterFArchive& operator<<(FArchive& Ar, FMyCustomData& Data)
{
Ar << Data.Name << Data.Value << Data.Timestamp;
return Ar;
}FArchiveSaveCompressedProxyFArchiveLoadCompressedProxySerialization/ArchiveSaveCompressedProxy.h// Compress
TArray<uint8> Compressed;
FArchiveSaveCompressedProxy Comp(Compressed, NAME_Zlib);
Comp.Serialize(RawData.GetData(), RawData.Num());
Comp.Flush();
// Decompress
FArchiveLoadCompressedProxy Decomp(Compressed, NAME_Zlib);
TArray<uint8> Raw;
Raw.SetNum(KnownUncompressedSize);
Decomp.Serialize(Raw.GetData(), Raw.Num());FArchiveSaveCompressedProxyFArchiveLoadCompressedProxySerialization/ArchiveSaveCompressedProxy.h// 压缩
TArray<uint8> Compressed;
FArchiveSaveCompressedProxy Comp(Compressed, NAME_Zlib);
Comp.Serialize(RawData.GetData(), RawData.Num());
Comp.Flush();
// 解压
FArchiveLoadCompressedProxy Decomp(Compressed, NAME_Zlib);
TArray<uint8> Raw;
Raw.SetNum(KnownUncompressedSize);
Decomp.Serialize(Raw.GetData(), Raw.Num());Serialize(FArchive& Ar)void UMyObject::Serialize(FArchive& Ar)
{
Super::Serialize(Ar); // always call Super first
Ar << BinaryField;
Ar << UniqueRunID;
if (Ar.IsLoading() && Ar.IsError()) { /* handle corruption */ }
}Serialize(FArchive& Ar)void UMyObject::Serialize(FArchive& Ar)
{
Super::Serialize(Ar); // 务必先调用父类方法
Ar << BinaryField;
Ar << UniqueRunID;
if (Ar.IsLoading() && Ar.IsError()) { /* 处理数据损坏 */ }
}namespace ESaveVersion
{
enum Type : int32
{
Initial = 0,
AddedInventory = 1,
SoftRefForWeapon = 2,
VersionPlusOne,
Latest = VersionPlusOne - 1
};
}
void USaveManager::RunMigrations(UMyGameSaveGame* Save)
{
if (Save->SaveVersion == ESaveVersion::Latest) { return; }
if (Save->SaveVersion < ESaveVersion::AddedInventory)
Save->InventoryItems.Reset();
if (Save->SaveVersion < ESaveVersion::SoftRefForWeapon)
{ /* convert old FName field to FSoftObjectPath */ }
Save->SaveVersion = ESaveVersion::Latest; // stamp after migration
}namespace ESaveVersion
{
enum Type : int32
{
Initial = 0,
AddedInventory = 1,
SoftRefForWeapon = 2,
VersionPlusOne,
Latest = VersionPlusOne - 1
};
}
void USaveManager::RunMigrations(UMyGameSaveGame* Save)
{
if (Save->SaveVersion == ESaveVersion::Latest) { return; }
if (Save->SaveVersion < ESaveVersion::AddedInventory)
Save->InventoryItems.Reset();
if (Save->SaveVersion < ESaveVersion::SoftRefForWeapon)
{ /* 将旧的FName字段转换为FSoftObjectPath */ }
Save->SaveVersion = ESaveVersion::Latest; // 迁移完成后更新版本号
}// Declare version enum + GUID (generate once with FGuid::NewGuid(), then hardcode)
struct FMySaveVersion
{
enum Type { Initial = 0, AddedQuestData = 1, VersionPlusOne, Latest = VersionPlusOne - 1 };
static const FGuid GUID;
};
const FGuid FMySaveVersion::GUID(0xA1B2C3D4, 0xE5F60718, 0x293A4B5C, 0x6D7E8F90);
// Register globally (module startup or static):
FCustomVersionRegistration GReg(FMySaveVersion::GUID, FMySaveVersion::Latest, TEXT("MySave"));
// In Serialize():
Ar.UsingCustomVersion(FMySaveVersion::GUID);
const int32 Ver = Ar.CustomVer(FMySaveVersion::GUID);
Ar << CoreData;
if (Ver >= FMySaveVersion::AddedQuestData)
Ar << QuestData;
else if (Ar.IsLoading())
QuestData.Reset(); // Initialize missing data on old saves// 声明版本枚举 + GUID(使用FGuid::NewGuid()生成一次后硬编码)
struct FMySaveVersion
{
enum Type { Initial = 0, AddedQuestData = 1, VersionPlusOne, Latest = VersionPlusOne - 1 };
static const FGuid GUID;
};
const FGuid FMySaveVersion::GUID(0xA1B2C3D4, 0xE5F60718, 0x293A4B5C, 0x6D7E8F90);
// 全局注册(模块启动时或静态注册):
FCustomVersionRegistration GReg(FMySaveVersion::GUID, FMySaveVersion::Latest, TEXT("MySave"));
// 在Serialize()中使用:
Ar.UsingCustomVersion(FMySaveVersion::GUID);
const int32 Ver = Ar.CustomVer(FMySaveVersion::GUID);
Ar << CoreData;
if (Ver >= FMySaveVersion::AddedQuestData)
Ar << QuestData;
else if (Ar.IsLoading())
QuestData.Reset(); // 在旧存档中初始化缺失的数据Serialize()void FMyStruct::Serialize(FArchive& Ar)
{
Ar.UsingCustomVersion(FMySaveVersion::GUID);
if (Ar.CustomVer(FMySaveVersion::GUID) < FMySaveVersion::RenamedHealthToHP)
{
float OldHealth;
Ar << OldHealth;
HP = OldHealth; // Migrate old field name to new
}
else
{
Ar << HP;
}
}Serialize()void FMyStruct::Serialize(FArchive& Ar)
{
Ar.UsingCustomVersion(FMySaveVersion::GUID);
if (Ar.CustomVer(FMySaveVersion::GUID) < FMySaveVersion::RenamedHealthToHP)
{
float OldHealth;
Ar << OldHealth;
HP = OldHealth; // 将旧字段名的数据迁移到新字段
}
else
{
Ar << HP;
}
}UCLASS()
class MYGAME_API UMyGameUserSettings : public UGameUserSettings
{
GENERATED_BODY()
public:
UPROPERTY(Config, BlueprintReadWrite, Category="Game")
float MasterVolume = 1.0f;
UPROPERTY(Config, BlueprintReadWrite, Category="Game")
bool bSubtitlesEnabled = true;
void ApplyAndSave() { ApplySettings(false); SaveSettings(); }
};
// Register in DefaultEngine.ini:
// [/Script/Engine.Engine]
// GameUserSettingsClassName=/Script/MyGame.MyGameUserSettingsUCLASS()
class MYGAME_API UMyGameUserSettings : public UGameUserSettings
{
GENERATED_BODY()
public:
UPROPERTY(Config, BlueprintReadWrite, Category="Game")
float MasterVolume = 1.0f;
UPROPERTY(Config, BlueprintReadWrite, Category="Game")
bool bSubtitlesEnabled = true;
void ApplyAndSave() { ApplySettings(false); SaveSettings(); }
};
// 在DefaultEngine.ini中注册:
// [/Script/Engine.Engine]
// GameUserSettingsClassName=/Script/MyGame.MyGameUserSettingsUCLASS(Config=Game, DefaultConfig, meta=(DisplayName="My Game Settings"))
class MYGAME_API UMyProjectSettings : public UDeveloperSettings
{
GENERATED_BODY()
public:
UPROPERTY(Config, EditAnywhere, Category="Save") int32 MaxSaveSlots = 5;
UPROPERTY(Config, EditAnywhere, Category="Save") bool bEnableAutoSave = true;
UPROPERTY(Config, EditAnywhere, Category="Save") float AutoSaveIntervalSeconds = 300.f;
static const UMyProjectSettings* Get() { return GetDefault<UMyProjectSettings>(); }
};UCLASS(Config=Game, DefaultConfig, meta=(DisplayName="My Game Settings"))
class MYGAME_API UMyProjectSettings : public UDeveloperSettings
{
GENERATED_BODY()
public:
UPROPERTY(Config, EditAnywhere, Category="Save") int32 MaxSaveSlots = 5;
UPROPERTY(Config, EditAnywhere, Category="Save") bool bEnableAutoSave = true;
UPROPERTY(Config, EditAnywhere, Category="Save") float AutoSaveIntervalSeconds = 300.f;
static const UMyProjectSettings* Get() { return GetDefault<UMyProjectSettings>(); }
};#include "Misc/ConfigCacheIni.h"
FString Value;
GConfig->GetString(TEXT("/Script/MyGame.MyConfig"), TEXT("Key"), Value, GGameIni);
GConfig->SetString(TEXT("/Script/MyGame.MyConfig"), TEXT("Key"), TEXT("Val"), GGameIni);
GConfig->Flush(/*bRemoveFromCache=*/false, GGameIni);
MyObject->SaveConfig(); // writes UPROPERTY(Config) fields to .ini
MyObject->LoadConfig(); // reloads from .ini[/Script/ModuleName.ClassName]SaveConfig()LoadConfig()OverrideConfigSection(FString& SectionName)#include "Misc/ConfigCacheIni.h"
FString Value;
GConfig->GetString(TEXT("/Script/MyGame.MyConfig"), TEXT("Key"), Value, GGameIni);
GConfig->SetString(TEXT("/Script/MyGame.MyConfig"), TEXT("Key"), TEXT("Val"), GGameIni);
GConfig->Flush(/*bRemoveFromCache=*/false, GGameIni);
MyObject->SaveConfig(); // 将UPROPERTY(Config)字段写入.ini文件
MyObject->LoadConfig(); // 从.ini文件重新加载[/Script/ModuleName.ClassName]SaveConfig()LoadConfig()OverrideConfigSection(FString& SectionName)// Platform save systems (Steam, EOS, console) provide ISaveGameSystem
// Access via IPlatformFeaturesModule:
ISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem();
if (SaveSystem && SaveSystem->DoesSaveSystemSupportMultipleUsers())
{
// Platform handles cloud sync — use UGameplayStatics normally
// Steam: auto-syncs Saved/SaveGames/ via Steam Cloud if configured in Steamworks
// EOS: use IOnlineSubsystem → IOnlineTitleFileInterface for explicit cloud read/write
}
// Cross-platform pattern: serialize to TArray<uint8>, then write via platform API
TArray<uint8> SaveData;
FMemoryWriter Ar(SaveData);
SaveObject->Serialize(Ar);
// Upload SaveData via platform SDK
// Steam Cloud — write save slot directly via Steamworks API
ISteamRemoteStorage* SteamStorage = SteamRemoteStorage();
if (SteamStorage && SteamStorage->IsCloudEnabledForApp())
{
SteamStorage->FileWrite("SaveSlot1.sav", SaveData.GetData(), SaveData.Num());
}
// Read back: SteamStorage->FileRead("SaveSlot1.sav", Buffer, Size)// 平台存档系统(Steam、EOS、主机)提供ISaveGameSystem
// 通过IPlatformFeaturesModule访问:
ISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem();
if (SaveSystem && SaveSystem->DoesSaveSystemSupportMultipleUsers())
{
// 平台处理云同步 — 正常使用UGameplayStatics即可
// Steam:若在Steamworks中配置,会自动同步Saved/SaveGames/目录
// EOS:使用IOnlineSubsystem → IOnlineTitleFileInterface进行显式云读写
}
// 跨平台模式:序列化为TArray<uint8>,再通过平台API写入
TArray<uint8> SaveData;
FMemoryWriter Ar(SaveData);
SaveObject->Serialize(Ar);
// 通过平台SDK上传SaveData
// Steam Cloud — 通过Steamworks API直接写入存档槽位
ISteamRemoteStorage* SteamStorage = SteamRemoteStorage();
if (SteamStorage && SteamStorage->IsCloudEnabledForApp())
{
SteamStorage->FileWrite("SaveSlot1.sav", SaveData.GetData(), SaveData.Num());
}
// 读取:SteamStorage->FileRead("SaveSlot1.sav", Buffer, Size)// Use FAES for symmetric encryption of save data
#include "Misc/AES.h"
// Build a zero-padded 32-byte FAESKey from a string.
// Do NOT use Key.Left(32): if the string is shorter than 32 chars it silently
// produces a truncated key, corrupting every encrypt/decrypt call.
static FAESKey MakeAESKey(const FString& KeyString)
{
FAESKey AESKey;
FMemory::Memzero(AESKey.Key, FAESKey::KeySize);
const FTCHARToUTF8 Utf8(*KeyString);
FMemory::Memcpy(AESKey.Key, Utf8.Get(), FMath::Min(Utf8.Length(), FAESKey::KeySize));
return AESKey;
}
void EncryptSaveData(TArray<uint8>& Data, const FString& KeyString)
{
int32 PaddedSize = Align(Data.Num(), FAES::AESBlockSize);
Data.SetNumZeroed(PaddedSize);
FAES::EncryptData(Data.GetData(), PaddedSize, MakeAESKey(KeyString));
}
void DecryptSaveData(TArray<uint8>& Data, const FString& KeyString)
{
FAES::DecryptData(Data.GetData(), Data.Num(), MakeAESKey(KeyString));
}// 使用FAES对存档数据进行对称加密
#include "Misc/AES.h"
// 从字符串构建零填充的32字节FAESKey。
// 请勿使用Key.Left(32):若字符串短于32字符,会静默生成截断密钥,导致所有加解密调用失败。
static FAESKey MakeAESKey(const FString& KeyString)
{
FAESKey AESKey;
FMemory::Memzero(AESKey.Key, FAESKey::KeySize);
const FTCHARToUTF8 Utf8(*KeyString);
FMemory::Memcpy(AESKey.Key, Utf8.Get(), FMath::Min(Utf8.Length(), FAESKey::KeySize));
return AESKey;
}
void EncryptSaveData(TArray<uint8>& Data, const FString& KeyString)
{
int32 PaddedSize = Align(Data.Num(), FAES::AESBlockSize);
Data.SetNumZeroed(PaddedSize);
FAES::EncryptData(Data.GetData(), PaddedSize, MakeAESKey(KeyString));
}
void DecryptSaveData(TArray<uint8>& Data, const FString& KeyString)
{
FAES::DecryptData(Data.GetData(), Data.Num(), MakeAESKey(KeyString));
}| Anti-Pattern | Problem | Fix |
|---|---|---|
Saving raw | Pointers invalid between sessions | Save |
| No version field | Adding/removing fields corrupts old saves silently | Always include |
| Blocks rendering, causes hitches | Use |
| Silent serialization failure | Add |
Ignoring | Reads past corrupted data, applies garbage | Check after every block; abort immediately if set |
| Overlapping async saves | Second save starts before first completes | Guard with |
| Hardcoded save file paths | Breaks on consoles and different platforms | Use |
<Project>/Saved/SaveGames/%LocalAppData%/<ProjectName>/Saved/SaveGames/UGameplayStatics::SaveGameToSlotISaveGameSystemFPaths::ProjectSavedDir()| 反模式 | 问题 | 修复方案 |
|---|---|---|
保存原始 | 指针在不同会话中无效 | 保存 |
| 无版本字段 | 添加/删除字段会静默损坏旧存档 | 始终包含 |
每帧在游戏线程调用 | 阻塞渲染,导致卡顿 | 使用 |
存档字段中的 | 序列化静默失败 | 为所有存档结构体添加 |
忽略 | 读取损坏数据后应用无效值 | 每次块操作后检查;若标记为错误则立即终止 |
| 异步保存重叠 | 第二次保存在第一次完成前启动 | 使用 |
| 硬编码存档文件路径 | 在主机和其他平台上失效 | 使用 |
<Project>/Saved/SaveGames/%LocalAppData%/<ProjectName>/Saved/SaveGames/UGameplayStatics::SaveGameToSlotISaveGameSystemFPaths::ProjectSavedDir()Ar.IsError()Slot_BackupSlot_PrimaryUSaveGame* LoadedSave = UGameplayStatics::LoadGameFromSlot(PrimarySlot, 0);
if (!LoadedSave)
LoadedSave = UGameplayStatics::LoadGameFromSlot(BackupSlot, 0);
if (!LoadedSave)
LoadedSave = UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass());Save_World_00Save_InventorySave_QuestsAsyncLoadGameFromSlotAGameModeULocalPlayerSaveGameAr.IsError()Slot_PrimarySlot_BackupUSaveGame* LoadedSave = UGameplayStatics::LoadGameFromSlot(PrimarySlot, 0);
if (!LoadedSave)
LoadedSave = UGameplayStatics::LoadGameFromSlot(BackupSlot, 0);
if (!LoadedSave)
LoadedSave = UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass());Save_World_00Save_InventorySave_QuestsAsyncLoadGameFromSlotAGameModeULocalPlayerSaveGamePublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" });
// For UDeveloperSettings:
PublicDependencyModuleNames.Add("DeveloperSettings");PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" });
// 若使用UDeveloperSettings:
PublicDependencyModuleNames.Add("DeveloperSettings");ue-cpp-foundationsue-data-assets-tablesue-gameplay-frameworkue-cpp-foundationsue-data-assets-tablesue-gameplay-frameworkreferences/save-system-architecture.mdreferences/save-system-architecture.md