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里,我们也需要同样的操作返回布尔值

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

相关推荐
SimonKing6 分钟前
艹,维护AI写的代码,我心态崩了......
java·后端·程序员
Momo__17 分钟前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
用户2986985301419 分钟前
Java Word 文档样式进阶:段落与文本背景色设置完全指南
java·后端
程序员小富23 分钟前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇23 分钟前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇23 分钟前
React中的forwardRef
前端·react.js·面试
槑有老呆32 分钟前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马34 分钟前
Verilog开发常见问题汇总解析
前端
子兮曰36 分钟前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端