"保存游戏"的含义可能因游戏不同而有很大差异,但大多数现代游戏的一般想法都是让玩家退出游戏,稍后再从中断的地方继续游戏。根据所开发的游戏类型,可能仅需一些基本信息即可实现此目的,例如玩家到达的最后一个检查点,以及玩家可能找到的物品。或者,也可能需要更详细的信息,可能涉及玩家与游戏内其他角色的社交互动列表,或各种请求、任务目标或次要情节的当前状态,等等。虚幻引擎4(UE4)有一个保存和加载系统,该系统涉及你创建的旨在满足游戏特定需求的一个或多个自定义 SaveGame 类,包括你需要在多个游戏会话中保留的所有信息。该系统允许保存多个游戏文件,并将不同的SaveGame类保存到这些文件。这对于将全局解锁的功能与特定于游戏通关的游戏数据分开非常有用。
创建SaveGame对象
要新建SaveGame对象,请新建蓝图类。弹出 选取父类(Pick Parent Class) 对话框时,展开 自定义类(Custom Classes) 下拉框,然后选择 SaveGame。可用搜索框直接跳至"SaveGame"。请将新蓝图命名为"MySaveGame"。

在新的SaveGame对象蓝图中,创建要保存所有信息的变量。
本例中还声明了部分将用于存储SaveSlotName和UserIndex默认值的变量, 因此保存到该SaveGame对象的各个类无需单独设置这些变量。此为可选步骤,若未修改默认值,将覆盖一个保存插槽。

编译蓝图后,可设置变量的默认值。
创建SaveGame对象
USaveGame
类会设置一个对象,这个对象可用作 Kismet/GameplayStatics.h
中声明的保存和加载函数的目标。
可使用C++类向导创建基于 USaveGame
的新类。

在本例中,新的 USaveGame
类名为 UMySaveGame
。为了使用该类,请将以下行添加到游戏模块的头文件中,放在任何其他 #include
指令之后:
MyProject.h
#include "MySaveGame.h"
#include "Kismet/GameplayStatics.h"
Header
在 SaveGame
对象的头文件中,可声明希望 SaveGame
存储的任何变量。
UPROPERTY(VisibleAnywhere, Category = Basic)
FString PlayerName;
本例中还声明了部分将用于存储 SaveSlotName
和 UserIndex
默认值的变量,
因此保存到该 SaveGame
对象的各个类无需单独设置这些变量。此为可选步骤,若未修改默认值,将覆盖一个保存插槽。
MySaveGame.h
#pragma once
#include "GameFramework/SaveGame.h"
#include "MySaveGame.generated.h"
/**
*
*/
UCLASS()
class [PROJECTNAME]_API UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY(VisibleAnywhere, Category = Basic)
FString PlayerName;
UPROPERTY(VisibleAnywhere, Category = Basic)
FString SaveSlotName;
UPROPERTY(VisibleAnywhere, Category = Basic)
uint32 UserIndex;
UMySaveGame();
};
源
通常,SaveGame
对象的源文件不需要任何特定代码即可运行,除非具体的保存系统具有要在此处设置的其他
功能。
本例在类构造函数中定义了 SaveSlotName
和 UserIndex
的值,因此其他Gameplay类可以读出和使用它们。
MySaveGame.cpp
// 版权所有1998-2018 Epic Games, Inc. 保留所有权利。
#include "[ProjectName].h"
#include "MySaveGame.h"
UMySaveGame::UMySaveGame()
{
SaveSlotName = TEXT("TestSaveSlot");
UserIndex = 0;
}
保存游戏
创建SaveGame类后,可向该类填充变量以存储游戏数据。例如,可创建一个整型变量来存储玩家的分数,或者创建一个字符串变量来存储玩家的姓名。保存游戏时,会将该信息从当前游戏世界转移到SaveGame对象,而加载游戏时,会将该信息从SaveGame对象复制到游戏对象(如角色、玩家控制器或游戏模式)。
首先,使用 创建保存游戏对象(Create Save Game Object) 节点,基于SaveGame类创建对象。确保将 保存游戏类(Save Game Class) 输入引脚的下拉框设为刚创建的类,本例中为 MySaveGame。创建保存游戏对象(Create Save Game Object) 节点将自动更改其输出引脚类型,使指定的类型与 保存游戏类(Save Game Class) 输入引脚匹配。这样,无需 类型转换为(Cast To) 节点便可直接进行使用。建议使用 提升为变量(Promote to Variable) 将结果对象保存至变量,以便之后重复使用刚创建的对象。

现在 保存游戏实例(Save Game Instance) 已包含自定义SaveGame对象,可向其发送信息。例如,可将 玩家名称(Player Name) 字段设为"PlayerOne"。继续设置SaveGame对象中的字段,直至包括需要存储在保存的游戏文件中的所有数据。

完全填充SaveGame对象后,利用 将游戏异步保存到插槽(Async Save Game To Slot) 节点完成游戏的保存。还需提供文件名和用户ID。本例中的文件名和用户ID为之前创建的默认值。执行过程将从顶部引脚立即开始,然后在SaveGame操作完成后从第二个引脚开始执行。执行第二个引脚前,输出引脚无效。

即便在保存大量数据时,将游戏异步保存到插槽(Async Save Game To Slot) 也能避免卡顿,因此推荐使用此方法保存游戏。但是,若SaveGame数据较小或在菜单或暂停屏幕中进行保存,则可改用 将游戏保存到插槽(Save Game To Slot) 节点保存游戏,如下所示。

以下屏幕截图显示了用MySaveGame类保存游戏的完整蓝图过程:
点击上图以查看大图。
首先,调用 CreateSaveGameObject
(来自 UGameplayStatics
库)以获取新的 UMySaveGame
对象。拥有该对象后,可向其中填充要保存的数据。最后,调用 SaveGameToSlot
或 AsyncSaveGameToSlot
将数据写出到设备。
异步保存
推荐的游戏保存方法是 AsyncSaveGameToSlot
。异步运行可防止突然的帧率卡顿,让玩家不容易注意到,并避免在某些平台上可能出现的认证问题。保存过程完成后将使用插槽名称、用户索引和指示成功或失败状态的 bool
值来调用委托(类型为 FAsyncSaveGameToSlotDelegate
)。
if (UMySaveGame* SaveGameInstance = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass())))
{
// 设置(可选的)委托。
FAsyncSaveGameToSlotDelegate SavedDelegate;
// USomeUObjectClass::SaveGameDelegateFunction是一个void函数,接受以下参数:const FString& SlotName、const int32 UserIndex、bool bSuccess
SavedDelegate.BindUObject(SomeUObjectPointer, &USomeUObjectClass::SaveGameDelegateFunction);
// 在SaveGame对象上设置数据。
SaveGameInstance->PlayerName = TEXT("PlayerOne");
// 启动异步保存过程。
UGameplayStatics::AsyncSaveGameToSlot(SaveGameInstance, SlotNameString, UserIndexInt32, SavedDelegate);
}
同步保存
对于小型的SaveGame格式,以及在暂停时或菜单中保存游戏,SaveGameToSlot
可以满足要求。它也非常易于使用,因为它只需立即保存游戏并返回指示成功或失败状态的 bool
值。如果需要保存大量数据,或者要在玩家仍与游戏世界积极交互时自动保存游戏,AsyncSaveGameToSlot
则是更好的选择。
if (UMySaveGame* SaveGameInstance = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass())))
{
// 在SaveGame对象上设置数据。
SaveGameInstance->PlayerName = TEXT("PlayerOne");
// 立即保存数据。
if (UGameplayStatics::SaveGameToSlot(SaveGameInstance, SlotNameString, UserIndexInt32))
{
// 保存成功。
}
}
二进制保存
可使用 SaveGameToMemory
函数将SaveGame对象转移到内存。此函数仅提供同步操作,但比保存到驱动器更快。调用方提供对用于存储数据的缓冲区(TArray<uint8>&
)的引用。成功后,该函数返回true。
TArray<uint8> OutSaveData;
if (UGameplayStatics::SaveGameToMemory(SaveGameObject, OutSaveData))
{
// 操作成功,OutSaveData现在包含SaveGame对象的二进制表示。
}
与 SaveGameToSlot
函数类似,还可使用缓冲区(const TArray<uint8>&
)以及插槽名称和用户ID信息来调用 SaveDataToSlot
,将二进制数据直接保存到文件中。与 SaveGameToMemory
一样,此函数仅提供同步操作,并返回指示成功或失败状态的 bool
值。
if (UGameplayStatics::SaveDataToSlot(InSaveData, SlotNameString, UserIndexInt32))
{
// 操作成功,InSaveData已写入到由我们提供的插槽名称和用户ID定义的保存文件。
}
在开发平台上,保存的游戏文件使用 .sav
扩展名,并显示在项目的 Saved\SaveGames
文件夹中。在其他平台(尤其是游戏主机)上,为适应特定的文件系统,这会有所不同。
加载游戏
要加载已保存的游戏,必须提供保存游戏时使用的保存插槽名称和用户ID。如果指定的SaveGame存在,引擎将向SaveGame对象中填充其包含的数据,并将其作为基础SaveGame(USaveGame
类)对象返回。然后,可将该对象的类型转换回自定义SaveGame类并访问数据。根据SaveGame类型包含的数据种类,你可能希望保留其副本,或者只是使用数据并丢弃对象。
与保存一样,可同步或异步加载。如果数据量很大,或者希望在加载时使用加载屏幕或动画,我们建议使用非同步方法。对于可快速加载的少量数据,则可选用同步方法。
要同步加载,请使用 从插槽加载游戏(Load Game From Slot) 节点。该节点直接明了,当提供的插槽名称和用户ID辨识出有效的SaveGame文件时,该节点将返回有效的SaveGame对象。执行加载操作时,游戏将停止。

从插槽异步加载游戏(Async Load Game From Slot) 的工作原理大致相同,但具有两个执行输出引脚。加载操作开始时将执行第一个引脚,加载操作完成时则执行第二个引脚。执行第二引脚前,变量输出引脚无效。成功(Success) 引脚可表示加载操作是否成功,但你也可将返回的对象传递到 是有效的(Is Valid) 节点,或将 类型转换为(Cast To) 节点中的失败视为加载过程中出现的错误总计。

异步加载
使用 AsyncLoadGameFromSlot
进行异步加载时,必须提供回调委托才能接收系统加载的数据。
// 设置委托。
FAsyncLoadGameFromSlotDelegate LoadedDelegate;
// USomeUObjectClass::LoadGameDelegateFunction是一个void函数,接受以下参数:const FString& SlotName、const int32 UserIndex、USaveGame* LoadedGameData
LoadedDelegate.BindUObject(SomeUObjectPointer, &USomeUObjectClass::LoadGameDelegateFunction);
UGameplayStatics::AsyncLoadGameFromSlot(SlotName, 0, LoadedDelegate);
同步加载
LoadGameFromSlot
函数将创建 USaveGame
对象,并在成功时返回该对象。
// 检索USaveGame对象并将其类型转换为UMySaveGame。
if (UMySaveGame* LoadedGame = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0)))
{
// 操作成功,因此LoadedGame现在包含之前保存的数据。
UE_LOG(LogTemp, Warning, TEXT("LOADED: %s"), *LoadedGame->PlayerName);
}
二进制加载
可使用 LoadDataFromSlot
以原始二进制形式从文件中加载SaveGame数据。此函数与 LoadGameFromSlot
非常相似,只不过它不创建SaveGame对象。对于这种类型的加载,只能使用同步操作。
TArray<uint8> OutSaveData;
if (UGameplayStatics::LoadDataFromSlot(OutSaveData, SlotNameString, UserIndexInt32))
{
// 操作成功,OutSaveData现在包含SaveGame对象的二进制表示。
}
还可通过调用 LoadGameFromMemory
将此二进制数据转换为SaveGame对象。这是一个同步调用,成功时返回新的 USaveGame
对象,失败时返回null指针。
if (UMySaveGame* SaveGameInstance = Cast<UMySaveGame>(LoadGameFromMemory(InSaveData)))
{
// 操作成功,SaveGameInstance能够将类型转换为预期的类型(UMySaveGame)。
}