在前面,我们实现了角色升级相关的功能,在PlayerState上记录了角色的等级和经验值,并在变动时,通过委托广播的形式向外广播,然后在UI上,通过监听委托的变动,进行修改等级和经验值。
在这一篇里,我们实现一下对属性值的操作,原理和等的差不多,但是编写死不一样。属性值是在属性面板上的属性,它有一个专用的属性面板控制器。
我们的控制器的基类为RPGWidgetController.h,从基类分了两个子类,OverlayWidgetController.h一直显示在战斗界面的覆层的控制器,用于更新血量蓝量,技能,经验等级等。AttributeMenuWidgetController.h为专门为属性面板使用的控制器,我们在属性里设置了一个数组,用于对应属性标签和属性的对应,并在属性修改后,对其进行广播,用于属性面板的更新,虽然此内容与本篇无关,作为记录。
我们接下来,会在此控制器增加属性点变动的委托。
在PlayerState增加属性点记录
和等级相同,我们先增加对属性点的参数。
UPROPERTY(VisibleAnywhere, ReplicatedUsing=OnRep_AttributePoints)
int32 AttributePoints = 0;
以及属性点的变动委托
FOnPlayerStateChanged OnAttributePointsChangedDelegate; //属性点数变动委托
从服务器到客户端的同步函数
UFUNCTION()
void OnRep_AttributePoints(int32 OldAttributePoints) const; //服务器出现更改自动同步到本地函数 属性点
实现
void ARPGPlayerState::OnRep_AttributePoints(int32 OldAttributePoints) const
{
OnAttributePointsChangedDelegate.Broadcast(AttributePoints);
}
接着增加属性点的相关函数
//属性点
FORCEINLINE int32 GetAttributePoints() const {return AttributePoints;} //获取角色当前属性点
void AddToAttributePoints(int32 InAttributePoints); //增加属性点
void SetAttributePoints(int32 InAttributePoints); //设置当前属性点
在设置后广播委托
void ARPGPlayerState::AddToAttributePoints(const int32 InAttributePoints)
{
AttributePoints += InAttributePoints;
OnAttributePointsChangedDelegate.Broadcast(AttributePoints);
}
void ARPGPlayerState::SetAttributePoints(const int32 InAttributePoints)
{
AttributePoints = InAttributePoints;
OnAttributePointsChangedDelegate.Broadcast(AttributePoints);
}
控制器添加委托
为了方便增加玩家状态的委托,我们将之前放在OverlayWidgetController.h覆层控制器里面的委托,移到基类RPGWidgetController.h,这样它的子类都可以使用这个委托类型,防止重复定义。
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerStateChangedSignature, int32, NewValue); //当玩家状态该表回调类型
接下来,我们就可以在属性面板控制器AttributeMenuWidgetController.h里面声明属性点委托
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnPlayerStateChangedSignature AttributePointsChangedDelegate; //监听属性点的变化委托
在绑定委托这里,我们实现对PlayerState里的委托绑定匿名函数,用于UI使用
ARPGPlayerState* RPGPlayerState = CastChecked<ARPGPlayerState>(PlayerState);
//绑定PlayerState的属性点委托
RPGPlayerState->OnAttributePointsChangedDelegate.AddLambda([this](const int32 Points)
{
AttributePointsChangedDelegate.Broadcast(Points);
});
在初始化这里,我们也需要广播一次,在面板打开时,显示正确的属性点
const ARPGPlayerState* RPGPlayerState = CastChecked<ARPGPlayerState>(PlayerState);
AttributePointsChangedDelegate.Broadcast(RPGPlayerState->GetAttributePoints());
修改一次升多个等级
我们还未对一次提升两级或者三级时对角色的属性点奖励和技能点奖励进行设置,实现起来简单,我们只需要对提升的等级进行遍历,将所有的奖励应用即可。
在AS里面,我们在升级时增加遍历,将已经升到的等级的奖励应用
//获取获得经验后的新等级
const int32 NewLevel = IPlayerInterface::Execute_FindLevelForXP(Props.SourceCharacter, CurrentXP + LocalIncomingXP);
const int32 NumLevelUps = NewLevel - CurrentLevel; //查看等级是否有变化
if(NumLevelUps > 0)
{
//如果连升多级,我们通过for循环获取每个等级的奖励
for(int32 i = CurrentLevel; i < NewLevel; i++)
{
//获取升级提供的技能点和属性点
const int32 AttributePointsReward = IPlayerInterface::Execute_GetAttributePointsReward(Props.SourceCharacter, i);
const int32 SpellPointsReward = IPlayerInterface::Execute_GetSpellPointsReward(Props.SourceCharacter, i);
//增加角色技能点和属性点
IPlayerInterface::Execute_AddToAttributePoints(Props.SourceCharacter, AttributePointsReward);
IPlayerInterface::Execute_AddToSpellPoints(Props.SourceCharacter, SpellPointsReward);
}
//提升等级
IPlayerInterface::Execute_AddToPlayerLevel(Props.SourceCharacter, NumLevelUps);
//播放升级效果
IPlayerInterface::Execute_LevelUp(Props.SourceCharacter);
//将血量和蓝量填充满
SetHealth(GetMaxHealth());
SetMana(GetMaxMana());
}
在UI实现对属性点的监听
我们之前创建的属性面板,以及对应的部件无法实现当前功能
所以,我们要为属性点创建一个专用的控制面板
在控件里面,我们需要通过蓝图修改属性名称,直接写上即可
然后修改监听的委托,修改为监听属性点的变动委托
现在我们有了属性点的值,接着要在玩家角色类里,将之前未完成的函数编写完成
void ARPGHero::AddToAttributePoints_Implementation(int32 InAttributePoints)
{
ARPGPlayerState* PlayerStateBase = GetPlayerState<ARPGPlayerState>();
check(PlayerStateBase); //检测是否有效,无限会暂停游戏
PlayerStateBase->AddToAttributePoints(InAttributePoints);
}
测试,在角色等级提升时,角色属性点是否会自动修改。
设置加点按钮的开启关闭
接下来,我们实现一下对加点按钮的开启和关闭的设置。我们想在有可分配的属性点时,加点按钮可以点击,而在可分配属性点数为0时,我们将其设置为不可点击。
我们在WBP_TextValueButtonRow里增加一个函数,用于设置按钮点击是否开启
然后在WBP_AttributeMenu里增加一个函数,传入当前可分配的属性点数,根据属性点数设置是个按钮是否需要开启
接着,我们重写事件,将获取到的将控制器存储到变量,并监听属性点变动回调,调用设置按钮函数,并在最后初始化。
这个获取方式是我们写的蓝图函数库的方法,它们设置的位置是我们自定义的HUD类上,并存储一份单例
接着运行测试,在可分配属性点为0时,无法点击
当拥有了可分配属性点,增加按钮变为可点击状态
实现属性加点功能
有了可分配属性,接下来我们将实现属性加点的功能。
我们将通过点击按钮触发事件,然后调用控制器内的函数,将加点属性的标签传递,接着在ASC里,通过发送事件,将需要增加的属性和之前增长经验的方式增加,并在内部处理可分配的属性值。
首先,我们在AttributeMenuWidgetController.h增加一个函数,用于增加事件触发后的调用函数
UFUNCTION(BlueprintCallable, Category="GAS|Attributes")
void UpgradeAttribute(const FGameplayTag& AttributeTag); //升级属性
然后在自定义的ASC中增加两个函数,它们其实是为了实现一个功能,但是对于属性操作只需要在服务器去做修改即可,所以,我们额外增加了一个只有服务器运行的函数
void UpgradeAttribute(const FGameplayTag& AttributeTag); //升级属性
UFUNCTION(Server, Reliable)
void ServerUpgradeAttribute(const FGameplayTag& AttributeTag); //服务器升级属性函数
我们需要能够在ASC里面去获取当前角色是否拥有可分配的属性点数,在角色接口里增加一个获取的函数
UFUNCTION(BlueprintNativeEvent)
int32 GetAttributePoints() const; //获取可分配属性点数
在角色基类里覆写它
virtual int32 GetSpellPoints_Implementation() const override;
实现此函数,从PlayerState里获取即可
int32 ARPGHero::GetAttributePoints_Implementation() const
{
const ARPGPlayerState* PlayerStateBase = GetPlayerState<ARPGPlayerState>();
check(PlayerStateBase); //检测是否有效,无限会暂停游戏
return PlayerStateBase->GetAttributePoints();
}
有了属性点数的获取,我们就可以实现ASC中创建的升级属性的函数,如果角色拥有可分配的点数,那么我们将让服务器执行发送事件,我们之前创建了被动技能接收属性相关的标签事件,我们将需要提升的属性标签和提升数量传递过去,并通过接口,减少相应的数量。
void URPGAbilitySystemComponent::UpgradeAttribute(const FGameplayTag& AttributeTag)
{
//判断Avatar是否基础角色接口
if(GetAvatarActor()->Implements<UPlayerInterface>())
{
//判断是否用于可分配点数
if(IPlayerInterface::Execute_GetAttributePoints(GetAvatarActor()) > 0)
{
ServerUpgradeAttribute(AttributeTag); //调用服务器升级属性
}
}
}
void URPGAbilitySystemComponent::ServerUpgradeAttribute_Implementation(const FGameplayTag& AttributeTag)
{
FGameplayEventData Payload; //创建一个事件数据
Payload.EventTag = AttributeTag;
Payload.EventMagnitude = 1.f;
//向自身发送事件,通过被动技能接收属性加点
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(GetAvatarActor(), AttributeTag, Payload);
//判断Avatar是否基础角色接口
if(GetAvatarActor()->Implements<UPlayerInterface>())
{
IPlayerInterface::Execute_AddToAttributePoints(GetAvatarActor(), -1); //减少一点可分配属性点
}
}
最后,我们在控制器里,调用ASC对应的函数
void UAttributeMenuWidgetController::UpgradeAttribute(const FGameplayTag& AttributeTag)
{
URPGAbilitySystemComponent* ASC = CastChecked<URPGAbilitySystemComponent>(AbilitySystemComponent);
ASC->UpgradeAttribute(AttributeTag);
}
接着编译代码,打开被动技能应用的GE,我们之前在里面增加了经验的设置
由于设置对应的属性是通过标签设置的,我们接着增加四个主要属性的修改,如果没有设置对应的属性,那么传递的值将默认为0
只要事件传递过来,带有标签和数值,GE会通过SetbyCaller被设置。
实现按钮的点击事件
最后,我们想给点击事件绑定回调,在回调里调用控制器的升级属性函数,并将属性标签传递,我们在里面已经监听对应标签的属性变动的回调了,标签是设置在UI上的,已经有了标签,后续就是实现点击回调调用函数即可。
我们首先在WBP_TextValueRow蓝图,将控制器切换并设置为变量,这样方便后续和子蓝图的使用
然后在WBP_AttributePointsRow里,对按钮的点击回调进行绑定,调用属性面板控制器的升级属性
这样,我们实现了整个逻辑,现在就可以去测试
修复无法满血的bug
出现这个bug的原因是因为在我们设置满血的时候,最大血量或者最大蓝量的属性还没有应用。解决这个bug需要在属性变动的回调里面修改,我们可以根据比例将属性值进行提升,或者设置一个变量,在属性变动时,进行修改。
如果按比例修改,我们可以按照官方的RPG进行修改,这里设置一下设置变量修改。
我们创建两个变量,在升级时设置将其设置为true
//将血量和蓝量填充满, 我们将设置变量
bFillHealth = true;
bFillMana = true;
覆写PostAttributeChange,它会在属性变动时触发
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override; //属性变动后回调函数
在里面如果最大血量或者蓝量变动,并且变量为true,那么我们将蓝量或者血量填满
void URPGAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
{
Super::PostAttributeChange(Attribute, OldValue, NewValue);
if(Attribute == GetMaxHealthAttribute() && bFillHealth)
{
SetHealth(GetMaxHealth());
bFillHealth = false;
}
if(Attribute == GetMaxManaAttribute() && bFillMana)
{
SetMana(GetMaxMana());
bFillMana = false;
}
}