【UE4C++-ActionRougelike-23】保存游戏状态
SaveGame:
用于创建和管理游戏保存数据。它提供了一个基础类,允许您创建一个自定义的保存类,用于存储和加载游戏进度、设置、角色状态等信息
GameplayStatics:提供了许多实用功能的静态类,包括了许多与游戏世界和游戏对象交互的常用方法,例如游戏保存和加载。
- CreateSaveGameObject():创建一个继承自
USaveGame
类的实例,用于保存游戏数据。 - SaveGameToSlot():将
USaveGame
实例中的游戏数据保存到指定槽位。 - LoadGameFromSlot():从指定槽位加载保存的游戏数据。
- DoesSaveGameExist():检查指定槽位中是否存在保存的游戏数据。
一、保存游戏
1、创建SaveGame实例
以SaveGame为父类创建游戏保存游戏类MySaveGame,
UCLASS()
class ACTIONROGUELIKE_API UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
// 用于存储游戏中的Credits数据
UPROPERTY()
int32 Credits;
};
2、保存和读取SaveGame
在MyGameModeBase中进行以下修改:
- 增加一个MySaveGame对象CurrentSaveGame表示当前游戏存档的对象和SlotName表示游戏存档名;
- 重写InitGame方法,用于游戏初始化
- 增加两个游戏存档方法WriteSaveGame和LoadSaveGame,分别用于写入和加载游戏存档
UCLASS()
class ACTIONROGUELIKE_API AMyGameModeBase : public AGameModeBase
{
protected:
// 用于保存游戏存档的槽位名称
FString SlotName;
// 当前的游戏存档对象
UPROPERTY()
UMySaveGame* CurrentSaveGame;
public:
// 重写AGameModeBase的InitGame方法,用于游戏初始化
void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override;
// 将游戏数据写入存档
UFUNCTION(BlueprintCallable, Category = "SaveGame")
void WriteSaveGame();
// 加载游戏存档的函数
void LoadSaveGame();
};
在MyGameModeBase中的构造函数中对SlotName初始化,并实现和游戏存档相关的三个方法
// 用于游戏初始化
void AMyGameModeBase::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
// 在游戏初始化时加载游戏存档
LoadSaveGame();
}
// 将游戏数据写入存档
void AMyGameModeBase::WriteSaveGame()
{
// 使用UGameplayStatics的SaveGameToSlot方法将当前游戏存档对象保存到指定的槽位
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SlotName, 0);
}
// 加载游戏存档
void AMyGameModeBase::LoadSaveGame()
{
// 判断指定槽位是否存在存档
if (UGameplayStatics::DoesSaveGameExist(SlotName, 0))
{
// 从指定槽位加载游戏存档
CurrentSaveGame = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0));
if (CurrentSaveGame == nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("Failed to load SaveGame Data."));
return;
}
UE_LOG(LogTemp, Log, TEXT("Loaded SaveGame Data."));
}
else
{
// 若指定槽位不存在存档,则创建新的存档对象
CurrentSaveGame = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
UE_LOG(LogTemp, Log, TEXT("Created New SaveGame Data."));
}
}
3、保存游戏按键绑定
在PlayerController_BP中进行保存游戏按键绑定
4、效果演示
二、保存玩家积分
SaveGame()
- 创建一个SaveGame实例
- 从PlayerState中复制积分到SaveGame
- 调用UGameStatics::SaveGameToSlot()
LoadGame()
- 检查SaveGame文件是否存在
- 调用UGameStatics::LoadGameFromSlot()
- 从SaveGame中复制积分到PlayerState
1、多人游戏下的保存游戏
//MyGameModeBase.h
//处理新玩家加入游戏的逻辑。当新玩家连接到游戏服务器时,服务器会调用此函数
void HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer) override;
//MyGameModeBase.cpp
void AMyGameModeBase::WriteSaveGame()
{
// 遍历GameState中的PlayerArray
for (int32 i = 0; i < GameState->PlayerArray.Num(); i++)
{
AMyPlayerState* PS = Cast<AMyPlayerState>(GameState->PlayerArray[i]);
if (PS)
{
// 如果成功获取到PlayerState,调用PS的SavePlayerState方法,传入当前的SaveGame对象
PS->SavePlayerState(CurrentSaveGame);
break; // 目前只支持单人游戏,所以处理一个玩家后就退出循环
}
}
// 将当前游戏存档对象保存到指定的槽位
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SlotName, 0);
}
//有新玩家加入时,首先为其加载其PlayerState
void AMyGameModeBase::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
// 获取NewPlayer的PlayerState,并存储到PS变量中
AMyPlayerState* PS = NewPlayer->GetPlayerState<AMyPlayerState>();
if (PS)
{
//调用PS的LoadPlayerState方法,传入当前的SaveGame对象
PS->LoadPlayerState(CurrentSaveGame);
}
}
2、保存和加载PlayerState
在MyPlayerState中增加保存和加载PlayerState的方法
class ACTIONROGUELIKE_API AMyPlayerState : public APlayerState
{
// 用于保存玩家状态
UFUNCTION(BlueprintNativeEvent)
void SavePlayerState(UMySaveGame* SaveObject);
// 用于加载玩家状态
UFUNCTION(BlueprintNativeEvent)
void LoadPlayerState(UMySaveGame* SaveObject);
}
void AMyPlayerState::SavePlayerState_Implementation(UMySaveGame* SaveObject)
{
if (SaveObject)
{
// 将玩家的Credits保存到SaveObject中
SaveObject->Credits = Credits;
}
}
void AMyPlayerState::LoadPlayerState_Implementation(UMySaveGame* SaveObject)
{
if (SaveObject)
{
// 从SaveObject中加载玩家的Credits
Credits = SaveObject->Credits;
}
}
3、效果演示
这里需要先修改Credits_Widget,每次创建时先初始化Credits值,方法类似于先前的Health
三、保存游戏可移动Actor的位置
SaveGame()
- 遍历World中的Actors
- 根据接口、类、标签等查找相关的角色。
- 保存一个包含ActorName和ActorTransform的结构体
- 添加结构体到SaveGame
LoadGame()
- 加载Actor数据(ActorName和ActorTransform的数组)
- 遍历World中的Actors
- 从可用的SaveGame数据中按名称查找匹配的角色
- 移动每个Actor到它保存的位置(Actor->SetActorTransform(LoadedTransform))
1、SaveGame中添加Actor数据
在SaveGame中要保存一个包含ActorName和ActorTransform的结构体,创建这样的一个结构体ActorSaveData,并保存一个结构体数组SavedActors
USTRUCT()
struct FActorSaveData
{
GENERATED_BODY()
public:
// 用于表示该结构体所属的Actor的名称
UPROPERTY()
FString ActorName;
// 用于存储可移动Actor的位置、旋转和缩放信息
UPROPERTY()
FTransform Transform;
};
UCLASS()
class ACTIONROGUELIKE_API UMySaveGame : public USaveGame
{
public:
// 用于存储游戏中多个Actor的保存数据
UPROPERTY()
TArray<FActorSaveData> SavedActors;
};
2、写入和加载Actor数据
在MyGameModeBase的写入SaveGame和加载SaveGame中,添加对SavedActors的写入和加载
void AMyGameModeBase::WriteSaveGame()
{
// 遍历GameState中的PlayerArray
//...
// 清空SavedActors数组,避免受先前保存的影响
CurrentSaveGame->SavedActors.Empty();
// 遍历整个世界的Actor
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
// 只关心实现USInteractionInterface接口的"游戏内Actor"
if (!Actor->Implements<USInteractionInterface>())
{
continue;
}
// 为每个Actor创建一个FActorSaveData实例
FActorSaveData ActorData;
ActorData.ActorName = Actor->GetName();
ActorData.Transform = Actor->GetActorTransform();
// 将ActorData添加到CurrentSaveGame的SavedActors数组中
CurrentSaveGame->SavedActors.Add(ActorData);
}
// 将当前游戏存档对象保存到指定的槽位
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SlotName, 0);
}
void AMyGameModeBase::LoadSaveGame()
{
// 判断指定槽位是否存在存档
if (UGameplayStatics::DoesSaveGameExist(SlotName, 0))
{
// 从指定槽位加载游戏存档
//...
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
// 只关心实现USInteractionInterface接口的"游戏内Actor"
if (!Actor->Implements<USInteractionInterface>())
{
continue;
}
// 遍历保存的Actor数据
for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
{
// 检查Actor名称是否匹配
if (ActorData.ActorName == Actor->GetName())
{
// 设置Actor的位置、旋转和缩放
Actor->SetActorTransform(ActorData.Transform);
break;
}
}
}
}
else
{
// 若指定槽位不存在存档,则创建新的存档对象
CurrentSaveGame = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
UE_LOG(LogTemp, Log, TEXT("Created New SaveGame Data."));
}
}
3、效果演示
这里拿实现ISInteractionInterface的宝箱作为测试的Actor(需打开宝箱蓝图中的模拟物理)
四、序列化保存任何数据
将所需变量标记为UPROPERTY(SaveGame)
- 例如变量bLidOpened
SaveGame()
- 遍历World中的Actors
- 根据接口、类、标签等查找相关的角色。
- Actor->Serialize(),将UPROPERTY(SaveGame)变量转换为FArchive(二进制)。
- 将包含序列化Actor数据的生成二进制数据添加到保存游戏中。
LoadGame()
- 从SaveGame中获取二进制数据
- 遍历World中的Actors
- 从可用的SaveGame数据中按名称查找匹配的角色
- Actor->Serialize(),将二进制数据转换为Actor数据
这里拿宝箱开启状态举例
1、标记变量
在STreasureChestItem.h中将bLidOpened属性标记为SaveGame
// SaveGame 标签,它是一个特定的 UPROPERTY 参数,用于表示该变量将在保存游戏时被序列化并存储在磁盘上
UPROPERTY(ReplicatedUsing="OnRep_LidOpened", BlueprintReadOnly, SaveGame)
bool bLidOpened;
2、写入和加载变量
在MyGameModeBase的写入SaveGame和加载SaveGame中,添加对变量的序列化以及之后的写入和加载
void AMyGameModeBase::WriteSaveGame()
{
//...
// 遍历整个世界的Actor
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
// 只关心实现USInteractionInterface接口的"游戏内Actor"
if (!Actor->Implements<USInteractionInterface>())
{
continue;
}
// 为每个Actor创建一个FActorSaveData实例
FActorSaveData ActorData;
ActorData.ActorName = Actor->GetName();
ActorData.Transform = Actor->GetActorTransform();
// 将ActorData的ByteData数组传递给内存缓冲区MemWriter
FMemoryWriter MemWriter(ActorData.ByteData);
// 创建一个以对象和名称为字符串的代理存档
FObjectAndNameAsStringProxyArchive Ar(MemWriter, true);
// 表示当前的反序列化操作是为了加载游戏
Ar.ArIsSaveGame = true;
// 将带有SaveGame标签的UPROPERTY转换为二进制数组
Actor->Serialize(Ar);
// 将ActorData添加到CurrentSaveGame的SavedActors数组中
CurrentSaveGame->SavedActors.Add(ActorData);
}
// 将当前游戏存档对象保存到指定的槽位
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SlotName, 0);
}
void AMyGameModeBase::LoadSaveGame()
{
// 判断指定槽位是否存在存档
if (UGameplayStatics::DoesSaveGameExist(SlotName, 0))
{
// ...
for (FActorIterator It(GetWorld()); It; ++It)
{
//...
// 遍历保存的Actor数据
for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
{
// 检查Actor名称是否匹配
if (ActorData.ActorName == Actor->GetName())
{
// 设置Actor的位置、旋转和缩放
Actor->SetActorTransform(ActorData.Transform);
// 将ActorData的ByteData数组传递给内存缓冲区MemReader
FMemoryReader MemReader(ActorData.ByteData);
// 创建一个以对象和名称为字符串的代理存档
FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
// 表示当前的反序列化操作是为了加载游戏
Ar.ArIsSaveGame = true;
// 将带有SaveGame标签的UPROPERTY转换为二进制数组
Actor->Serialize(Ar);
// 设置Actor的位置、旋转和缩放(这一步要等到下面完成初始化加载函数后进行)
ISInteractionInterface::Execute_OnActorLoaded(Actor);
break;
}
}
}
}
else
{
// 若指定槽位不存在存档,则创建新的存档对象
//...
}
}
3、宝箱初始化加载
在InteractionInterface.h中创建函数OnActorLoaded用于Actor的初始化加载,在宝箱类STreasureChestItem中添加OnActorLoad的实现
//InteractionInterface.h
UFUNCTION(BlueprintNativeEvent)
void OnActorLoaded();
//STreasureChestItem.h
void OnActorLoaded_Implementation();
//STreasureChestItem.cpp
void ASTreasureChestItem::OnActorLoaded_Implementation()
{
OnRep_LidOpened();
}
4、开启复制
勾选宝箱的Replicate Movement和RootComponent中的Component Replicates。
- Replicate Movement 主要用于在多人游戏中同步角色或其他移动对象的位置、旋转和速度。当为一个对象启用 Replicate Movement 时,引擎会自动处理位置、旋转和速度的同步,确保游戏中的所有客户端看到的对象位置和动作保持一致。
- Component Replicates 用于同步 Actor 组件的状态。当为一个组件启用 Component Replicates 时,引擎会自动处理组件属性的同步,确保所有客户端看到的组件状态保持一致。
5、效果演示
6、总结
6.1 UPROPERTY(SaveGame)
- 概念:
UPROPERTY(SaveGame)
是虚幻引擎中一个特殊的UPROPERTY
标签,它用于标记在保存游戏进度时需要序列化和存储的类成员变量。当使用此标签时,引擎会确保在游戏保存和加载时,这些变量的值得到正确地存储和恢复。 - 使用:只需在类成员变量声明之前添加此宏
- 注意事项:确保保存类继承自
USaveGame
,这样引擎才能正确处理保存和加载逻辑
6.2 FMemoryWriter和FMemoryReader
概念
FMemoryWriter
类用于将序列化数据写入内存缓冲区。FMemoryReader
类用于从内存缓冲区中读取序列化数据。
使用
// FMemoryWriter用法 // 创建一个 TArray<uint8> 对象,用于存储序列化数据。 // 创建一个 FMemoryWriter 对象,将数据写入 `TArray<uint8>` 缓冲区。 // 使用 FMemoryWriter 对象将对象(如Actor)序列化到内存缓冲区中 // 创建一个TArray对象,用于存储序列化数据 TArray<uint8> SerializedData; // 创建一个FMemoryWriter对象,将数据写入SerializedData缓冲区 FMemoryWriter MemoryWriter(SerializedData, true); // 使用FMemoryWriter对象将Actor对象序列化到内存缓冲区中 MyActor->Serialize(MemoryWriter); // FMemoryReader用法 //从文件或其他来源获取序列化数据,并存储在 TArray<uint8> 对象中。 //创建一个 FMemoryReader 对象,从 TArray<uint8> 缓冲区中读取数据。 //使用 FMemoryReader 对象从内存缓冲区中反序列化对象(如Actor)。 // 从文件等读取序列化数据 TArray<uint8> SerializedData; // 创建一个 FMemoryReader 对象,从 SerializedData 缓冲区中读取数据 FMemoryReader MemoryReader(SerializedData, true); // 使用FMemoryReader对象从内存缓冲区中反序列化Actor对象 MyActor->Serialize(MemoryReader);
注意事项
- 在序列化和反序列化过程中,确保对象的属性已经标记为
UProperty
,否则反射系统无法识别这些属性 - 序列化后的数据存储在
TArray<uint8>
对象中,需要确保该对象在序列化和反序列化过程中一直存在。在性能敏感的场景中,可以考虑预先分配足够的内存空间,以避免动态扩展数组带来的性能开销
- 在序列化和反序列化过程中,确保对象的属性已经标记为
6.3 FObjectAndNameAsStringProxyArchive
- 概念:
FObjectAndNameAsStringProxyArchive
是Unreal Engine中的一个代理序列化类,它继承自FArchive
类。它的主要作用是在序列化和反序列化过程中,将对象引用和名字引用以字符串形式存储。
6.4 ArIsSaveGame
- 概念:
ArIsSaveGame
是虚幻引擎中FArchive
类的一个成员变量,当序列化操作是为了保存游戏时,它的值为true
,否则为false
6.5 Serialize
- 概念:
Serialize()
函数是虚幻引擎中用于实现对象序列化和反序列化的核心方法,用于处理对象的序列化和反序列化。通过序列化,我们可以将对象的状态转换为二进制数据以便存储或传输。反序列化则是将二进制数据转换回对象的状态。 - 使用:
- 在序列化过程中,
Serialize()
函数将对象的状态(成员变量)写入FArchive
对象。 - 在反序列化过程中,
Serialize()
函数从FArchive
对象中读取数据,并将其应用到对象的状态。
- 在序列化过程中,