110. UE5 GAS RPG 实现玩家角色数据存档
在这篇,我们实现将玩家数据保存到存档内。
增加保存玩家属性
玩家属性默认的等级,经验值,可分配的技能点和属性点。还有一些角色基础属性也需要保存,回忆一下,我们是如何实现玩家的属性的,我们是通过多个GE去实现的玩家的属性,将其分为了基础属性、额外属性和最后填充当前的血量和蓝量。基础属性首先有一些默认的属性,通过Instant类型的GE应用,后期属性增长可以通过分配属性点去实现,额外属性是基于基础属性计算得出,所以这些属性不需要去保存到存档,所以我们只需要保存基础属性值即可。
我们在SaveGame类里增加对应的需要保存的属性
//经验值
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。
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);
}
}
现在我们有了对应的配置,在创建存档时,我们就可以设置它的等级了。
在存档视图模型里设置玩家等级属性
//角色的等级
UPROPERTY(BlueprintReadOnly, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess)) //meta=(AllowPrivateAccess)允许设置私有,但在蓝图公开
int32 PlayerLevel;
创建Get和Set函数
void SetPlayerLevel(const int32 InPlayerLevel);
int32 GetPlayerLevel() const { return PlayerLevel; };
设置时,并触发广播,在UI上使用对应的部件,我们就可以同步更新
void UMVVM_LoadSlot::SetPlayerLevel(const int32 InPlayerLevel)
{
UE_MVVM_SET_PROPERTY_VALUE(PlayerLevel, InPlayerLevel);
}
后面编译代码,在UI上,记得绑定
接着,我们在加载界面视图模型里创建新存档时,设置存档玩家等级
在加载存档时,修改存档视图模型的等级
加载存档角色属性
我们现在实现了保存,那么,接下来,将实现从存档读取角色的属性并设置回去。
我们接下来在存档里设置一个值,这个值用于记录当前角色是否为第一加载存档,因为在加载界面创建的角色还没有设置初始属性
//第一次加载存档
UPROPERTY()
bool bFirstTimeLoadIn = true;
考虑到从存档读取玩家信息方法有可能后续在别的地方使用,我们将其设置为一个函数库函数,之前,我们将角色的基本属性初始值设置到的GE里,是写死的,每个角色初始值是不同的,如果在存档读取,则需要从存档读取值然后设置,那么我们需要一个SetByCaller的GE来实现对玩家属性的定义。
我们在之前定义敌人使用初始化数据的类里额外增加一项,这一项是可以在GameMode或者PlayerState里获取到的
//主要属性,玩家的基础属性,通过SetByCaller设置
UPROPERTY(EditDefaultsOnly, Category="Common Class Defaults")
TSubclassOf<UGameplayEffect> PrimaryAttributes_SetByCaller;
然后就是设置玩家的次级属性和额外属性的GE,我们已经设置到了玩家角色身上,没必要在别的地方再设置一次,实现获取,我们可以在玩家接口里实现两个获取函数。在玩家接口类IPlayerInterface里,我们增加两个函数,用于获取对应的GE
//获取角色使用的次级属性GameplayEffect
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
TSubclassOf<UGameplayEffect> GetSecondaryAttributes();
//获取角色使用的额外属性GameplayEffect
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
TSubclassOf<UGameplayEffect> GetVitalAttributes();
然后在玩家基类里覆写
virtual TSubclassOf<UGameplayEffect> GetSecondaryAttributes_Implementation() override;
virtual TSubclassOf<UGameplayEffect> GetVitalAttributes_Implementation() override;
直接返回在玩家身上设置的对应类即可
TSubclassOf<UGameplayEffect> ARPGHero::GetSecondaryAttributes_Implementation()
{
return DefaultSecondaryAttributes;
}
TSubclassOf<UGameplayEffect> ARPGHero::GetVitalAttributes_Implementation()
{
return DefaultVitalAttributes;
}
现在,实现读取存档所需的GE已经可以都获取,我们可以在函数库实现对应的函数。
我们在函数库实现一个通过存档初始化角色属性的函数,需要传入角色,ASC和读取的存档
/**
* 从存档初始化角色的属性
*
* @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设置玩家的其它属性,我们就实现了对应的函数。
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());
}
}
接下来,我们在玩家角色类里增加一个新的函数,用于从存档里读取玩家信息并设置。
//角色加载存档保存的数值
void LoadProgress() const;
然后我们取消之前在初始化ASC后设置属性的函数调用
然后在PossessedBy函数里,取消初始化技能,并在初始化完成ASC后,调用加载存档设置角色。PossessedBy只在服务器端执行。
接着我们将实现读取存档获取玩家属性,获取到PlayerState设置等级,经验,可分配的属性点和技能点。这里它没通过bFirstTimeLoadIn区分的原因是因为我们创建存档时,默认值就是初始值,所以可以直接设置。
然后我们判断是否为第一次加载存档(之前没有保存过存档数据),如果是第一次,我们将通过默认值初始化,也就是之前默认的那一套,然后初始化默认的角色技能。
如果玩家之前通过检查点保存过数据,我们通过函数库函数通过存档设置数据,然后调用默认初始化技能(方便测试)。
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等级后,触发委托广播,我们需要使用一个新的委托类型,可以返回当前是否需要触发等级提升广播。
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnPlayerLevelChanged, int32, bool); //等级变动委托,返回新的等级和是否显示升级弹框
修改等级变动委托类型
FOnPlayerLevelChanged OnLevelChangedDelegate; //等级变动委托
设置等级这里可以在调用函数时,选择设置
/**
* 设置当前等级
* @param InLevel 新的等级
* @param bLevelUp 当前是否提升了等级
*/
void SetLevel(int32 InLevel, const bool bLevelUp);
AddToLevel是在AS里等级提升后,会调用此函数,所以,我们广播返回true
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里,我们也需要同样的操作返回布尔值
最后在蓝图里,通过此值判断来是否要触发界面等级提示信息。