快速结论
最容易混淆的一点:SaveGame 标记本身不会把对象保存到磁盘。它只是一个属性标记。保存到磁盘的是 UGameplayStatics::SaveGameToSlot、AsyncSaveGameToSlot 或你自己写的文件/字节流逻辑。
如果你只是做普通存档
创建一个继承自 USaveGame 的类,把要保存的数据作为 UPROPERTY 放进去,然后调用 SaveGameToSlot。这个流程是 UE 官方文档推荐的基本用法。
如果你想保存 Actor 字段
需要自己枚举目标 Actor,并用 ArIsSaveGame = true 的归档调用 Serialize。这时只有标了 SaveGame 的属性会被序列化。
SaveGame 是什么
在 C++ 里,正确写法是把 SaveGame 放在 UPROPERTY(...) 的说明符列表里:
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Save")
int32 Coins = 0;
它不是 metadata。下面这种写法表达的是元数据,不是这个属性说明符的正确用法:
// 不要这样写
UPROPERTY(EditAnywhere, meta = (SaveGame))
int32 Coins = 0;
引擎层面上,SaveGame 对应的是属性 flag。普通序列化不会因为它自动发生变化;当归档的 IsSaveGame() 为真时,UE 的属性序列化会跳过没有 SaveGame 标记的字段。
属性写成 UPROPERTY(SaveGame)。
UHT 给属性打上 SaveGame flag。
自定义归档设置 ArIsSaveGame = true。
序列化时只保留带该 flag 的字段。
标准 USaveGame 用法
最常见的存档方式是定义一个 USaveGame 子类。把角色等级、背包数据、关卡进度、设置等可重建的数据放进去,保存时填充对象,读取时再把数据应用回游戏对象。
SaveGame 类
// MyPlayerSaveGame.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "MyPlayerSaveGame.generated.h"
USTRUCT(BlueprintType)
struct FInventoryEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
FName ItemId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
int32 Count = 0;
};
UCLASS()
class UMyPlayerSaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, SaveGame, Category = "Player")
int32 PlayerLevel = 1;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, SaveGame, Category = "Player")
FTransform PlayerTransform;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, SaveGame, Category = "Inventory")
TArray<FInventoryEntry> Inventory;
};
保存
#include "Kismet/GameplayStatics.h"
#include "MyPlayerSaveGame.h"
void UMySaveSubsystem::SavePlayer(ACharacter* Player)
{
const FString SlotName = TEXT("Profile_0");
constexpr int32 UserIndex = 0;
UMyPlayerSaveGame* SaveObject = Cast<UMyPlayerSaveGame>(
UGameplayStatics::CreateSaveGameObject(UMyPlayerSaveGame::StaticClass()));
if (!SaveObject || !Player)
{
return;
}
SaveObject->PlayerLevel = CurrentLevel;
SaveObject->PlayerTransform = Player->GetActorTransform();
SaveObject->Inventory = BuildInventorySnapshot();
UGameplayStatics::SaveGameToSlot(SaveObject, SlotName, UserIndex);
}
读取
void UMySaveSubsystem::LoadPlayer(ACharacter* Player)
{
const FString SlotName = TEXT("Profile_0");
constexpr int32 UserIndex = 0;
UMyPlayerSaveGame* SaveObject = Cast<UMyPlayerSaveGame>(
UGameplayStatics::LoadGameFromSlot(SlotName, UserIndex));
if (!SaveObject || !Player)
{
return;
}
CurrentLevel = SaveObject->PlayerLevel;
Player->SetActorTransform(SaveObject->PlayerTransform);
ApplyInventorySnapshot(SaveObject->Inventory);
}
官方 API 文档对 SaveGameToSlot / SaveGameToMemory 有一个容易被忽略的说明:标准保存 USaveGame 对象时会写入非 transient 属性,SaveGame flag 本身不作为过滤条件。也就是说,在普通 USaveGame 类里,真正最低要求是字段必须是 UPROPERTY 且不能是 Transient。不过仍建议给存档字段加上 SaveGame,因为它能表达意图,也方便以后切到自定义归档或自动采集方案。
自定义 Archive 用法
当你想做“自动保存某些 Actor/Component 的字段”时,SaveGame 标记才是主角。思路是:你自己决定要保存哪些对象,然后创建一个 SaveGame 模式的归档,让对象执行 Serialize。
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
struct FMySaveGameArchive : public FObjectAndNameAsStringProxyArchive
{
explicit FMySaveGameArchive(FArchive& InnerArchive)
: FObjectAndNameAsStringProxyArchive(InnerArchive, true)
{
ArIsSaveGame = true;
}
};
保存 Actor 的标记字段
USTRUCT()
struct FSavedActorRecord
{
GENERATED_BODY()
UPROPERTY(SaveGame)
FName ActorName;
UPROPERTY(SaveGame)
FTransform Transform;
UPROPERTY(SaveGame)
TArray<uint8> Bytes;
};
FSavedActorRecord MakeActorRecord(AActor* Actor)
{
FSavedActorRecord Record;
Record.ActorName = Actor->GetFName();
Record.Transform = Actor->GetActorTransform();
FMemoryWriter Writer(Record.Bytes, true);
FMySaveGameArchive Archive(Writer);
Actor->Serialize(Archive);
return Record;
}
读取 Actor 的标记字段
void ApplyActorRecord(AActor* Actor, const FSavedActorRecord& Record)
{
if (!Actor)
{
return;
}
Actor->SetActorTransform(Record.Transform);
FMemoryReader Reader(Record.Bytes, true);
FMySaveGameArchive Archive(Reader);
Actor->Serialize(Archive);
}
这种方案下,Actor 内部只有标记了 SaveGame 的属性会被写入。没有标记的运行时缓存、组件引用、临时状态会自然被跳过。
UCLASS()
class ASavableChest : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
bool bOpened = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
TArray<FName> LootItemIds;
// 不保存:运行时缓存,下次可重建。
UPROPERTY(Transient)
TObjectPtr<UStaticMeshComponent> CachedMesh = nullptr;
};
Blueprint 里怎么勾
Blueprint 变量详情面板里的 SaveGame 复选框,设置的是同一个属性标记。它常在变量的 Advanced / 高级区域里。
- 普通 SaveGame 蓝图:创建继承自
SaveGame的蓝图类,把变量放进去,再用Create Save Game Object、Save Game To Slot、Load Game From Slot。 - 自动序列化 Actor:Actor 蓝图里的变量也要勾 SaveGame,并且仍然需要你的 C++ 或插件逻辑去枚举 Actor、创建 Archive、写入字节。
- Struct 变量:外层 struct 变量和 struct 内部真正需要保存的成员都建议标记。只标外层通常不够稳。
常见坑
| 问题 | 原因 | 建议 |
|---|---|---|
SaveGame 写成了 meta=(SaveGame) |
它是 property specifier,不是 metadata。 | 写成 UPROPERTY(EditAnywhere, SaveGame)。 |
| 以为勾了 SaveGame 就自动存盘 | 标记只是筛选条件,不是保存动作。 | 仍然要调用 SaveGameToSlot,或自己写文件/Archive 逻辑。 |
| 普通 C++ 成员变量没有保存 | UE 反射序列化只认识 UPROPERTY。 |
需要保存的字段必须进入反射系统。 |
| 保存 Actor 指针后读回来无效 | 运行时对象地址不能跨进程/跨启动保存。 | 保存稳定 ID、类路径、Transform、数据快照,加载时重建或查找对象。 |
| Struct 里部分字段丢失 | 自定义 SaveGame Archive 会按成员属性过滤。 | 外层字段和内部成员都使用 UPROPERTY(SaveGame)。 |
| 保存大型数据时卡顿 | 同步保存会占用主线程或触发明显 IO 等待。 | 较大存档优先用 AsyncSaveGameToSlot,并在回调里处理结果。 |
推荐存什么
- 玩家进度:等级、经验、任务状态、已解锁内容。
- 世界状态快照:Actor 稳定 ID、Transform、开关状态、生命值、掉落物数据。
- 资源引用:优先存
FSoftObjectPath、TSoftObjectPtr或资产 ID。 - 可重建数据:背包条目、装备 ID、数量、随机种子,而不是完整运行时对象图。
参考资料
- Unreal Engine UProperties:官方属性说明符列表,包含
SaveGame。 - Saving and Loading Your Game:官方 SaveGame 类、保存、读取流程。
- USaveGame API:
USaveGame是保存状态对象的基类。 - FArchive::IsSaveGame API:说明 Archive 是否处于保存/读取游戏状态的模式。
- GameplayStatics Python API:其中
save_game_to_slot说明标准保存会写出非 transient 属性,且不检查SaveGameflag。