在上一篇文章里,我们实现技能面板里的技能按钮配置数据,在角色对应的等级后,会解锁对应的技能,并实现了监听玩家角色所拥有的技能点数。
在这一篇里,我们将实现,通过玩家所拥有的技能点数,对技能进行升级
增加升级按钮
我们在技能按钮里增加两个按钮,用于实现技能等级的增加和减少功能
增加技能的选中功能
首先,我们在技能按钮中增加两个函数,分别是选中后显示选中动画,并将动画对应节点显示出来
然后就是取消对技能按钮的焦点,我们将升级按钮隐藏,并将焦点节点隐藏
然后我们在按钮的用户控件里增加一个事件分发器,用于在技能树控件里面监听回调
我们给技能按钮增加一个点击事件,并增加一个变量防止频繁点击,并调用事件分发器
这样,就可以在按钮被点击时,整个技能树可以根据其需求修改
实现技能按钮点击,取消其它技能按钮的焦点状态
现在,我们实现了按钮点击的状态,接下来,我们要实现,当前按钮点击时,只焦点当前按钮,并显示当前按钮的加点和减技能的按钮,其它按钮都取消掉对应的效果。
由于,我们技能分为主动技能和被动技能两个技能树,所以我们需要在两个技能树里面增加两个函数,用于清除树里的所有技能按钮到默认状态
并在树里增加一个事件分发器,用于通知上层处理多个树之间的交互
接着,我们在技能树里,对按钮的事件分发器进行监听,在树里的某个按钮被点击时,我们将树的所有状态恢复到默认,并调用事件分发器,发送通知
接着,我们在WBP_SpellMenu里,绑定两个树的事件分发器,这样,就实现了,在有一个树的按钮被点击时,所有树的按钮都可以被清除到默认状态
接下来运行测试效果
我们后续还有很多工作需要完成,首先显而易见的是被锁住的技能按钮无法激活焦点状态。然后就是技能的升级和降级按钮,需要在可按的时候显示出来,或者显示无法点击状态。
对技能按钮附加按钮状态更新
我们有了技能的升级按钮和降级按钮,后续,我们将通过这两个按钮实现对技能的等级修改。所以,我们需要一个委托用来更新按钮的状态,在技能等级低到无法降级时,我们降级按钮也无法实现点击,如果技能等级满级,或者没有可分配的技能点数时,将无法实现技能的升级。
接下来,我们先实现委托,并在委托中也返回技能是否可以装配变量。
首先,我们在SpellMenuWidgetController.h中,增加一个委托声明
cpp
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FSpellGlobeSelectedSignature, bool, bSpendPointsEnabled, bool, bEquipEnabled, bool, bDemotionPointsEnabled);
通过声明类型创建一个变量
cpp
UPROPERTY(BlueprintAssignable)
FSpellGlobeSelectedSignature SpellGlobeSelectedSignature; //选中技能按钮后,升级和装备按钮的变动回调
然后增加一个函数,在技能按钮被点击时,调用,并传入技能按钮上设置的标签,返回当前的技能状态标签,通过标签,我们可以设置上锁的技能按钮,不会播放焦点的动画
cpp
UFUNCTION(BlueprintCallable)
FGameplayTag SpellGlobeSelected(const FGameplayTag& AbilityTag); //技能按钮选中调用函数,处理升级按钮和装配
接着,我们创建一个私有函数,这个函数,通过技能实例的状态标签和玩家角色所拥有的可分配技能数,返回对应的按钮状态和技能可装配情况
cpp
private:
//通过技能状态标签和可分配技能点数来获取技能是否可以装配和技能是否可以升级
static void ShouldEnableButtons(const FGameplayTag& AbilityStatus, bool HasSpellPoints, bool& bShouldEnableSpellPoints, bool& bShouldEnableEquip, bool& bShouldDemotionPoints);
在SpellGlobeSelected实现这里,我们首先判断技能按钮上的标签,然后获取技能实例和可分配的技能点数,通过ShouldEnableButtons获取到当前技能的状态,并通过委托广播出去。
cpp
FGameplayTag USpellMenuWidgetController::SpellGlobeSelected(const FGameplayTag& AbilityTag)
{
const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();
const int32 SpellPoints = GetRPGPS()->GetSpellPoints(); //获取技能点数
FGameplayTag AbilityStatus;
const bool bTagValid = AbilityTag.IsValid(); //判断传入的标签是否存在
const bool bTagNone = AbilityTag.MatchesTag(GameplayTags.Abilities_None); //判断传入的是否为空技能标签
const FGameplayAbilitySpec* AbilitySpec = GetRPGASC()->GetSpecFromAbilityTag(AbilityTag); //通过技能标签获取技能
const bool bSpecValid = AbilitySpec != nullptr; //判断技能实例是否存在
if(!bTagValid || bTagNone || !bSpecValid)
{
//传入标签不存在,或传入的为空技能标签,或者技能实例不存在时,设置为技能按钮显示上锁状态
AbilityStatus = GameplayTags.Abilities_Status_Locked;
}
else
{
//从技能实例获取技能的状态标签
AbilityStatus = GetRPGASC()->GetStatusTagFromSpec(*AbilitySpec);
}
bool bEnableSpendPoints = false; //技能是否可以升级
bool bEnableEquip = false; //技能是否可以装配
bool bEnableDemotion = false; //技能是否可以降级
ShouldEnableButtons(AbilityStatus, SpellPoints > 0, bEnableSpendPoints, bEnableEquip, bEnableDemotion); //获取结果
SpellGlobeSelectedSignature.Broadcast(bEnableSpendPoints, bEnableEquip, bEnableDemotion); //广播状态
return AbilityStatus;
}
ShouldEnableButtons的实现,就是根据状态和是否拥有可分配的技能点数,来设置对应的状态
cpp
void USpellMenuWidgetController::ShouldEnableButtons(const FGameplayTag& AbilityStatus, bool HasSpellPoints, bool& bShouldEnableSpellPoints, bool& bShouldEnableEquip, bool& bShouldDemotionPoints)
{
const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();
if (AbilityStatus.MatchesTagExact(GameplayTags.Abilities_Status_Equipped))
{
bShouldEnableSpellPoints = HasSpellPoints;
bShouldEnableEquip = true;
bShouldDemotionPoints = true;
}
else if (AbilityStatus.MatchesTagExact(GameplayTags.Abilities_Status_Eligible))
{
bShouldEnableSpellPoints = HasSpellPoints;
bShouldEnableEquip = false;
bShouldDemotionPoints = false;
}
else if (AbilityStatus.MatchesTagExact(GameplayTags.Abilities_Status_Unlocked))
{
bShouldEnableSpellPoints = HasSpellPoints;
bShouldEnableEquip = true;
bShouldDemotionPoints = true;
}
else if (AbilityStatus.MatchesTagExact(GameplayTags.Abilities_Status_Locked))
{
bShouldEnableSpellPoints = false;
bShouldEnableEquip = false;
bShouldDemotionPoints = false;
}
}
函数实现后,我们在蓝图增加一个函数,讲委托获取的变量设置给对应的按钮,讲是否可装配的变量存储下来,方便后续使用
然后绑定委托
接下来,我们修改蓝图的Select函数,首先设置其选中,然后调用我们设置的SpellGlobeSelected函数获取当前技能的状态,如果是已经上锁的技能按钮,不需要显示±按钮。
接下来就是测试效果
技能升降级按钮的动态修改
我们实现了点击按钮触发技能按钮的升降级状态的更新,还未实现在技能在可分配技能点变动和技能状态变动时,对升降级按钮的更新。
为了实现这个功能,我们增加一个结构体
在技能面板控制器类,添加结构体,用于存储当前选中的技能标签和状态标签
cpp
//在技能面板选中的技能的标签结构体
struct FSelectedAbility
{
FGameplayTag Ability = FGameplayTag(); //技能标签
FGameplayTag Status = FGameplayTag(); //技能状态标签
};
在类里添加一个参数,并对值进行初始化
cpp
//设置选中技能默认值
FSelectedAbility SelectedAbility = {FRPGGameplayTags::Get().Abilities_None, FRPGGameplayTags::Get().Abilities_Status_Locked};
接着创建一个可分配技能点数的参数,用于存储可分配点数
cpp
//保存当前技能可分配点数
int32 CurrentSpellPoints = 0;
接着增加一个用于广播的函数,因为我们需要在多个地方调用
cpp
//广播当前技能按钮升降级按钮状态和可装配状态
void BroadcastSpellGlobeSelected() const;
函数实现就是之前的实现,只是值的获取改为了自身
cpp
void USpellMenuWidgetController::BroadcastSpellGlobeSelected() const
{
bool bEnableSpendPoints = false; //技能是否可以升级
bool bEnableEquip = false; //技能是否可以装配
bool bEnableDemotion = false; //技能是否可以降级
ShouldEnableButtons(SelectedAbility.Status, CurrentSpellPoints > 0, bEnableSpendPoints, bEnableEquip, bEnableDemotion); //获取结果
SpellGlobeSelectedSignature.Broadcast(bEnableSpendPoints, bEnableEquip, bEnableDemotion); //广播状态
}
并将SpellGlobeSelected函数修改为调用此函数广播
cpp
//选中时,更新控制器缓存数据
SelectedAbility.Ability = AbilityTag;
SelectedAbility.Status = AbilityStatus;
CurrentSpellPoints = SpellPoints;
BroadcastSpellGlobeSelected(); //广播升降级按钮状态
接着,在绑定数据监听广播里,在技能状态和可分配点数变动时,广播升降级按钮状态广播
cpp
void USpellMenuWidgetController::BindCallbacksToDependencies()
{
//绑定技能状态修改后的委托回调
GetRPGASC()->AbilityStatusChanged.AddLambda([this](const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag)
{
//技能状态修改,修改技能的升降级按钮的状态
if(SelectedAbility.Ability.MatchesTagExact(AbilityTag))
{
SelectedAbility.Status = StatusTag;
BroadcastSpellGlobeSelected(); //广播升降级按钮状态
}
//广播技能数据更新,用于更新技能按钮显示状态
if(AbilityInfo)
{
FRPGAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag); //获取到技能数据
Info.StatusTag = StatusTag;
AbilityInfoDelegate.Broadcast(Info);
}
});
//绑定技能点变动回调
GetRPGPS()->OnSpellPointsChangedDelegate.AddLambda([this](const int32 SpellPoints)
{
SpellPointChanged.Broadcast(SpellPoints); //广播拥有的技能点
CurrentSpellPoints = SpellPoints;
BroadcastSpellGlobeSelected(); //广播升降级按钮状态
});
}
实现技能的升级降级功能
接下来,我们要实现技能的升降级的功能。我们在技能面板控制器增加两个函数,一个用于升级按钮调用函数,另一个用于降级按钮调用函数。
cpp
UFUNCTION(BlueprintCallable)
void SpendPointButtonPressed(const FGameplayTag& AbilityTag); //升级按钮调用函数
UFUNCTION(BlueprintCallable)
void DemotionPointButtonPressed(const FGameplayTag& AbilityTag); //降级按钮调用函数
void USpellMenuWidgetController::SpendPointButtonPressed(const FGameplayTag& AbilityTag)
{
if(GetRPGASC())
{
GetRPGASC()->ServerSpendSpellPoint(AbilityTag); //调用ASC等级提升函数
}
}
void USpellMenuWidgetController::DemotionPointButtonPressed(const FGameplayTag& AbilityTag)
{
if(GetRPGASC())
{
GetRPGASC()->ServerDemotionSpellPoint(AbilityTag); //调用ASC降低技能等级
}
}
然后在自定义ASC中实现两个处理函数,这两个函数只在服务器端执行,然后将修改结果同步到客户端
cpp
UFUNCTION(Server, Reliable)
void ServerSpendSpellPoint(const FGameplayTag& AbilityTag); //只在服务器端运行,消耗技能点函数提升技能等级
UFUNCTION(Server, Reliable)
void ServerDemotionSpellPoint(const FGameplayTag& AbilityTag); //只在服务器端运行,降低技能返回技能点
在提升技能等级的函数里,我们首先判断技能实例是否存在防止出错,然后减少可分配技能点,根据技能的状态标签进行处理
如果技能状态是可解锁,那么,提升等级后,技能状态标签将修改为未解锁状态
将技能等级提升一级,并将技能状态设置为立即同步到客户端
cpp
void URPGAbilitySystemComponent::ServerSpendSpellPoint_Implementation(const FGameplayTag& AbilityTag)
{
//获取到技能实例
if( FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
{
//减少一个可分配的技能点
if(GetAvatarActor()->Implements<UPlayerInterface>())
{
IPlayerInterface::Execute_AddToSpellPoints(GetAvatarActor(), -1);
}
//获取状态标签
FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();
FGameplayTag StatusTag = GetStatusTagFromSpec(*AbilitySpec);
//根据状态标签处理
if(StatusTag.MatchesTagExact(GameplayTags.Abilities_Status_Eligible))
{
//技能升级,如果是可解锁状态,将状态标签从可解锁,切换为已解锁
AbilitySpec->DynamicAbilityTags.RemoveTag(GameplayTags.Abilities_Status_Eligible);
AbilitySpec->DynamicAbilityTags.AddTag(GameplayTags.Abilities_Status_Unlocked);
StatusTag = GameplayTags.Abilities_Status_Unlocked;
//提升技能等级
AbilitySpec->Level += 1;
}
else if(StatusTag.MatchesTagExact(GameplayTags.Abilities_Status_Equipped) || StatusTag.MatchesTagExact(GameplayTags.Abilities_Status_Unlocked))
{
AbilitySpec->Level += 1;
}
ClientUpdateAbilityStatus(AbilityTag, StatusTag, AbilitySpec->Level); //广播技能状态修改
MarkAbilitySpecDirty(*AbilitySpec); //设置当前技能立即复制到每个客户端
}
}
而在降级函数里,我们是增加一点可分配点数,并减少技能的等级
cpp
void URPGAbilitySystemComponent::ServerDemotionSpellPoint_Implementation(const FGameplayTag& AbilityTag)
{
//获取到技能实例
if( FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
{
//增加一个可分配的技能点
if(GetAvatarActor()->Implements<UPlayerInterface>())
{
IPlayerInterface::Execute_AddToSpellPoints(GetAvatarActor(), 1);
}
//获取状态标签
FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();
FGameplayTag StatusTag = GetStatusTagFromSpec(*AbilitySpec);
if(StatusTag.MatchesTagExact(GameplayTags.Abilities_Status_Equipped) || StatusTag.MatchesTagExact(GameplayTags.Abilities_Status_Unlocked))
{
AbilitySpec->Level -= 1;
if(AbilitySpec->Level < 1)
{
//技能小于1级,当前技能将无法装配,直接设置为可解锁状态
AbilitySpec->DynamicAbilityTags.RemoveTag(GameplayTags.Abilities_Status_Equipped);
AbilitySpec->DynamicAbilityTags.RemoveTag(GameplayTags.Abilities_Status_Unlocked);
AbilitySpec->DynamicAbilityTags.AddTag(GameplayTags.Abilities_Status_Eligible);
StatusTag = GameplayTags.Abilities_Status_Eligible;
}
}
ClientUpdateAbilityStatus(AbilityTag, StatusTag, AbilitySpec->Level); //广播技能状态修改
MarkAbilitySpecDirty(*AbilitySpec); //设置当前技能立即复制到每个客户端
}
}
而在里面的广播函数,是我们新增的函数,用于只在客户端广播的函数
cpp
UFUNCTION(Client, Reliable)
void ClientUpdateAbilityStatus(const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag, int32 AbilityLevel); //技能状态更新后回调
我们对其进行了修改,增加了技能等级的广播
cpp
void URPGAbilitySystemComponent::ClientUpdateAbilityStatus_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& StatusTag, const int32 AbilityLevel)
{
AbilityStatusChanged.Broadcast(AbilityTag, StatusTag, AbilityLevel);
}
接着我们在技能按钮上增加一个显示等级的文本框
设置选中时显示技能等级,未选中时,不再显示
在结构体增加了技能等级的缓存
cpp
//在技能面板选中的技能的标签结构体
struct FSelectedAbility
{
FGameplayTag Ability = FGameplayTag(); //技能标签
FGameplayTag Status = FGameplayTag(); //技能状态标签
int32 Level = 0; //技能等级
};
在回调里面设置设置等级
在控制器广播回调这里,我们也增加了一个对等级的广播
并在UI这里监听委托回函函数里修改技能等级
然后我们绑定技能按钮升降级按钮点击回调,调用控制器函数
接着测试