【UE4C++-ActionRougelike-23】保存游戏状态


【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(需打开宝箱蓝图中的模拟物理)

保存可移动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 对象中读取数据,并将其应用到对象的状态。

文章作者: Woilin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Woilin !
评论
  目录