110. UE5 GAS RPG 实现玩家角色数据存档

在这篇,我们实现将玩家数据保存到存档内。

增加保存玩家属性

玩家属性默认的等级,经验值,可分配的技能点和属性点。还有一些角色基础属性也需要保存,回忆一下,我们是如何实现玩家的属性的,我们是通过多个GE去实现的玩家的属性,将其分为了基础属性、额外属性和最后填充当前的血量和蓝量。基础属性首先有一些默认的属性,通过Instant类型的GE应用,后期属性增长可以通过分配属性点去实现,额外属性是基于基础属性计算得出,所以这些属性不需要去保存到存档,所以我们只需要保存基础属性值即可。

我们在SaveGame类里增加对应的需要保存的属性

cpp 复制代码
	//经验值
	UPROPERTY()
	int32 XP = 0;

	//可分配技能点
	UPROPERTY()
	int32 SpellPoints = 0;

	//可分配属性点
	UPROPERTY()
	int32 AttributePoints = 0;

	/************************** 主要属性 **************************/
	
	//力量
	UPROPERTY()
	float Strength = 0;

	//智力
	UPROPERTY()
	float Intelligence = 0;

	//韧性
	UPROPERTY()
	float Resilience = 0;

	//体力
	UPROPERTY()
	float Vigor = 0;

现在有了对应的参数,我们需要去实现对属性保存,然后保存的存档里。

在检查点和玩家产生碰撞后,会触发角色身上的SaveProgress函数。我们直接在里面增加对应的属性设置,角色常规属性我们在PlayerState上设置的,直接在PlayerState上设置即可。角色基础属性是保存在AS里,我们从AS里获取并设置到存档。

这里注意:如果通过检查点保存了存档,证明保存了当前的玩家操作后的数据,我们需要将bFirstTimeLoadIn 设置为false。

cpp 复制代码
void ARPGHero::SaveProgress_Implementation(const FName& CheckpointTag)
{
	if(const ARPGGameMode* GameMode = Cast<ARPGGameMode>(UGameplayStatics::GetGameMode(this)))
	{
		//获取存档
		ULoadScreenSaveGame* SaveGameData = GameMode->RetrieveInGameSaveData();
		if(SaveGameData == nullptr) return;

		//修改存档数据
		SaveGameData->PlayerStartTag = CheckpointTag;
		SaveGameData->ActivatedPlayerStatTags.AddUnique(CheckpointTag); //将检查点添加到已激活数组,并去重

		//修改玩家相关
		if(const ARPGPlayerState* RPGPlayerState = Cast<ARPGPlayerState>(GetPlayerState()))
		{
			SaveGameData->PlayerLevel = RPGPlayerState->GetPlayerLevel();
			SaveGameData->XP = RPGPlayerState->GetXP();
			SaveGameData->AttributePoints = RPGPlayerState->GetAttributePoints();
			SaveGameData->SpellPoints = RPGPlayerState->GetSpellPoints();
		}

		//修改主要属性
		SaveGameData->Strength = URPGAttributeSet::GetStrengthAttribute().GetNumericValue(GetAttributeSet());
		SaveGameData->Intelligence = URPGAttributeSet::GetIntelligenceAttribute().GetNumericValue(GetAttributeSet());
		SaveGameData->Resilience = URPGAttributeSet::GetResilienceAttribute().GetNumericValue(GetAttributeSet());
		SaveGameData->Vigor = URPGAttributeSet::GetVigorAttribute().GetNumericValue(GetAttributeSet());

		SaveGameData->bFirstTimeLoadIn = false; //保存完成将第一次加载属性设置为false

		//保存存档
		GameMode->SaveInGameProgressData(SaveGameData);
	}
}

现在我们有了对应的配置,在创建存档时,我们就可以设置它的等级了。

在存档视图模型里设置玩家等级属性

cpp 复制代码
	//角色的等级
	UPROPERTY(BlueprintReadOnly, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess)) //meta=(AllowPrivateAccess)允许设置私有,但在蓝图公开
	int32 PlayerLevel;

创建Get和Set函数

cpp 复制代码
	void SetPlayerLevel(const int32 InPlayerLevel);
	int32 GetPlayerLevel() const { return PlayerLevel; };

设置时,并触发广播,在UI上使用对应的部件,我们就可以同步更新

cpp 复制代码
void UMVVM_LoadSlot::SetPlayerLevel(const int32 InPlayerLevel)
{
	UE_MVVM_SET_PROPERTY_VALUE(PlayerLevel, InPlayerLevel);
}

后面编译代码,在UI上,记得绑定

接着,我们在加载界面视图模型里创建新存档时,设置存档玩家等级

在加载存档时,修改存档视图模型的等级

加载存档角色属性

我们现在实现了保存,那么,接下来,将实现从存档读取角色的属性并设置回去。

我们接下来在存档里设置一个值,这个值用于记录当前角色是否为第一加载存档,因为在加载界面创建的角色还没有设置初始属性

cpp 复制代码
	//第一次加载存档
	UPROPERTY()
	bool bFirstTimeLoadIn = true;

考虑到从存档读取玩家信息方法有可能后续在别的地方使用,我们将其设置为一个函数库函数,之前,我们将角色的基本属性初始值设置到的GE里,是写死的,每个角色初始值是不同的,如果在存档读取,则需要从存档读取值然后设置,那么我们需要一个SetByCaller的GE来实现对玩家属性的定义。

我们在之前定义敌人使用初始化数据的类里额外增加一项,这一项是可以在GameMode或者PlayerState里获取到的

cpp 复制代码
	//主要属性,玩家的基础属性,通过SetByCaller设置
	UPROPERTY(EditDefaultsOnly, Category="Common Class Defaults")
	TSubclassOf<UGameplayEffect> PrimaryAttributes_SetByCaller;

然后就是设置玩家的次级属性和额外属性的GE,我们已经设置到了玩家角色身上,没必要在别的地方再设置一次,实现获取,我们可以在玩家接口里实现两个获取函数。在玩家接口类IPlayerInterface里,我们增加两个函数,用于获取对应的GE

cpp 复制代码
	//获取角色使用的次级属性GameplayEffect
	UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
	TSubclassOf<UGameplayEffect> GetSecondaryAttributes();

	//获取角色使用的额外属性GameplayEffect
	UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
	TSubclassOf<UGameplayEffect> GetVitalAttributes();

然后在玩家基类里覆写

cpp 复制代码
	virtual TSubclassOf<UGameplayEffect> GetSecondaryAttributes_Implementation() override;
	virtual TSubclassOf<UGameplayEffect> GetVitalAttributes_Implementation() override;

直接返回在玩家身上设置的对应类即可

cpp 复制代码
TSubclassOf<UGameplayEffect> ARPGHero::GetSecondaryAttributes_Implementation()
{
	return DefaultSecondaryAttributes;
}

TSubclassOf<UGameplayEffect> ARPGHero::GetVitalAttributes_Implementation()
{
	return DefaultVitalAttributes;
}

现在,实现读取存档所需的GE已经可以都获取,我们可以在函数库实现对应的函数。

我们在函数库实现一个通过存档初始化角色属性的函数,需要传入角色,ASC和读取的存档

cpp 复制代码
	/**
	 * 从存档初始化角色的属性
	 *
	 * @param WorldContextObject 一个世界场景的对象,用于获取当前所在的世界
	 * @param ASC 角色的技能系统组件
	 * @param SaveGame 角色使用的存档指针
	 *
	 * @return void 
	 *
	 * @note 这个函数主要用于从存档里读取角色信息,并初始化
	 */
	UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|CharacterClassDefaults", meta=(DefaultToSelf = "WorldContextObject"))
	static void InitializeDefaultAttributesFromSaveData(const UObject* WorldContextObject, UAbilitySystemComponent* ASC, ULoadScreenSaveGame* SaveGame);

实现这里,获取到ASC的Avatar,我们将ASC的Avatar设置为了玩家角色实例,从数据资产里获取到SetByCaller的GE,通过SetByCaller设置角色的基础属性,然后通过玩家接口获取到次级属性的GE和额外属性的GE设置玩家的其它属性,我们就实现了对应的函数。

cpp 复制代码
void URPGAbilitySystemLibrary::InitializeDefaultAttributesFromSaveData(const UObject* WorldContextObject, UAbilitySystemComponent* ASC, ULoadScreenSaveGame* SaveGame)
{
	AActor* AvatarActor = ASC->GetAvatarActor();
	
	const FRPGGameplayTags& GameplayTags = FRPGGameplayTags::Get();

	//从实例获取到关卡角色的配置
	const UCharacterClassInfo* CharacterClassInfo = GetCharacterClassInfo(WorldContextObject);
	if(CharacterClassInfo == nullptr) return;

	//*********************************初始化主要属性*********************************

	//创建GE的上下文句柄
	FGameplayEffectContextHandle EffectContextHandle = ASC->MakeEffectContext();
	EffectContextHandle.AddSourceObject(AvatarActor);

	//根据句柄和类创建GE实例,并可以通过句柄找到GE实例
	const FGameplayEffectSpecHandle PrimaryContextHandle = ASC->MakeOutgoingSpec(CharacterClassInfo->PrimaryAttributes_SetByCaller, 1.0f, EffectContextHandle);

	//通过标签设置GE使用的配置
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(PrimaryContextHandle, GameplayTags.Attributes_Primary_Strength, SaveGame->Strength);
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(PrimaryContextHandle, GameplayTags.Attributes_Primary_Intelligence, SaveGame->Intelligence);
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(PrimaryContextHandle, GameplayTags.Attributes_Primary_Resilience, SaveGame->Resilience);
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(PrimaryContextHandle, GameplayTags.Attributes_Primary_Vigor, SaveGame->Vigor);

	//应用GE
	ASC->ApplyGameplayEffectSpecToSelf(*PrimaryContextHandle.Data.Get());

	if(AvatarActor->Implements<UPlayerInterface>())
	{
		//*********************************设置次级属性*********************************
	
		FGameplayEffectContextHandle SecondaryContextHandle = ASC->MakeEffectContext();
		SecondaryContextHandle.AddSourceObject(AvatarActor);
		const FGameplayEffectSpecHandle SecondarySpecHandle = ASC->MakeOutgoingSpec(IPlayerInterface::Execute_GetSecondaryAttributes(AvatarActor), 1.0f, SecondaryContextHandle);
		ASC->ApplyGameplayEffectSpecToSelf(*SecondarySpecHandle.Data.Get());

		//*********************************填充血量和蓝量*********************************

		FGameplayEffectContextHandle VitalContextHandle = ASC->MakeEffectContext();
		VitalContextHandle.AddSourceObject(AvatarActor);
		const FGameplayEffectSpecHandle VitalSpecHandle = ASC->MakeOutgoingSpec(IPlayerInterface::Execute_GetVitalAttributes(AvatarActor), 1.0f, VitalContextHandle);
		ASC->ApplyGameplayEffectSpecToSelf(*VitalSpecHandle.Data.Get());
	}
}

接下来,我们在玩家角色类里增加一个新的函数,用于从存档里读取玩家信息并设置。

cpp 复制代码
	//角色加载存档保存的数值
	void LoadProgress() const;

然后我们取消之前在初始化ASC后设置属性的函数调用

然后在PossessedBy函数里,取消初始化技能,并在初始化完成ASC后,调用加载存档设置角色。PossessedBy只在服务器端执行。

接着我们将实现读取存档获取玩家属性,获取到PlayerState设置等级,经验,可分配的属性点和技能点。这里它没通过bFirstTimeLoadIn区分的原因是因为我们创建存档时,默认值就是初始值,所以可以直接设置。

然后我们判断是否为第一次加载存档(之前没有保存过存档数据),如果是第一次,我们将通过默认值初始化,也就是之前默认的那一套,然后初始化默认的角色技能。

如果玩家之前通过检查点保存过数据,我们通过函数库函数通过存档设置数据,然后调用默认初始化技能(方便测试)。

cpp 复制代码
void ARPGHero::LoadProgress() const
{
	if(const ARPGGameMode* GameMode = Cast<ARPGGameMode>(UGameplayStatics::GetGameMode(this)))
	{
		//获取存档
		ULoadScreenSaveGame* SaveGameData = GameMode->RetrieveInGameSaveData();
		if(SaveGameData == nullptr) return;

		//修改玩家相关
		if(ARPGPlayerState* RPGPlayerState = Cast<ARPGPlayerState>(GetPlayerState()))
		{
			RPGPlayerState->SetLevel(SaveGameData->PlayerLevel, false);
			RPGPlayerState->SetXP(SaveGameData->XP);
			RPGPlayerState->SetAttributePoints(SaveGameData->AttributePoints);
			RPGPlayerState->SetSpellPoints(SaveGameData->SpellPoints);
		}

		//判断是否为第一次加载存档,如果第一次,属性没有相关内容
		if(SaveGameData->bFirstTimeLoadIn)
		{
			//如果第一次加载存档,使用默认GE初始化主要属性
			InitializeDefaultAttributes();

			//初始化角色技能
			AddCharacterAbilities();
		}
		else
		{
			//如果不是第一次,将通过函数库函数通过存档数据初始化角色属性
			URPGAbilitySystemLibrary::InitializeDefaultAttributesFromSaveData(this, AbilitySystemComponent, SaveGameData);

			//初始化角色技能 TODO:还未实现通过存档获取保存的技能,现在测试使用。
			AddCharacterAbilities();
		}
	}
}

我们之前实现方式是直接使用GE初始化,这个可以在首次进入游戏时使用,后续使用存档初始化时,将无法使用。

设置蓝图实现属性保存读取

代码编写完毕,我们编译代码打开UE设置对应蓝图。

通过之前创建的角色默认初始化GE我们复制一个创建一个通过SetByCaller设置角色主要属性。

然后将GE里的属性设置都修改为SetByCaller的方式

并设置每个属性对应的标签

然后我们设置到角色初始化属性的资产数据里,方便后续使用。

最后,我们运行,击杀几只小怪,等级提升后,查看属性

然后在新的检查点保存存档。

重新进入查看存档等级是否正确。

然后进入地牢查看属性是否正确设置回去。

解决通过存档进入游戏弹框问题

我们现在发现通过存档进入场景时,会触发升级提示,我们想取消这个。

我们需要修改委托,在设置PlayerState等级后,触发委托广播,我们需要使用一个新的委托类型,可以返回当前是否需要触发等级提升广播。

cpp 复制代码
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPlayerLevelChanged, int32, bool); //等级变动委托,返回新的等级和是否显示升级弹框

修改等级变动委托类型

cpp 复制代码
FOnPlayerLevelChanged OnLevelChangedDelegate; //等级变动委托

设置等级这里可以在调用函数时,选择设置

cpp 复制代码
	/**
	 * 设置当前等级
	 * @param InLevel 新的等级
	 * @param bLevelUp 当前是否提升了等级
	 */
	void SetLevel(int32 InLevel, const bool bLevelUp); 

AddToLevel是在AS里等级提升后,会调用此函数,所以,我们广播返回true

cpp 复制代码
void ARPGPlayerState::AddToLevel(const int32 InLevel)
{
	Level += InLevel;
	OnLevelChangedDelegate.Broadcast(Level, true);
}

void ARPGPlayerState::SetLevel(const int32 InLevel, const bool bLevelUp)
{
	Level = InLevel;
	OnLevelChangedDelegate.Broadcast(Level, bLevelUp);
}

void ARPGPlayerState::OnRep_Level(int32 OldLevel) const
{
	OnLevelChangedDelegate.Broadcast(Level, true); //上面修改委托只会在服务器触发,在此处设置是在服务器更新到客户端本地后触发
}

在主界面使用的Controller里,我们也需要同样的操作返回布尔值

最后在蓝图里,通过此值判断来是否要触发界面等级提示信息。

相关推荐
前端拾光者6 分钟前
前端数据可视化思路及实现案例
前端·数据库·信息可视化
程序猿小D6 分钟前
第三百三十一节 Java网络教程 - Java网络UDP多播
java·网络·udp
灭掉c与java10 分钟前
第五章springboot实现web的常用功能
java·spring boot·spring
初晴~21 分钟前
【Spring】RESTful设计风格
java·后端·spring·springboot·restful
沉默璇年22 分钟前
react中Fragment的使用场景
前端·react.js·前端框架
风动也无爱40 分钟前
Java的正则表达式和爬虫
java·爬虫·正则表达式
訴山海41 分钟前
解决Excel文件流读取数字为时间乱码问题
java·文件流
今日之风甚是温和1 小时前
【Excel】拆分多个sheet,为单一表格
java·excel·sheet·vb宏
聂 可 以1 小时前
IDEA一键启动多个微服务
java·微服务·intellij-idea
刘大浪1 小时前
IDEA 2024安装指南(含安装包以及使用说明 cannot collect jvm options 问题一)
java·ide·intellij-idea