在上一篇里,我们实现了在技能面板,点击技能能够显示出技能的相关描述以及下一级的技能的对应描述。
在这一篇里,我们实现一下技能的装配。
在之前,我们实现了点击按钮时,在技能面板控制器里存储了当前选中的技能的相关信息,有了这个信息以后,我们在实现装配时,可以使用这个数据进行处理。
当选中技能后,我们接着点击下面的技能插槽时,如果符合装配条件,我们将实现装配到对应的插槽。
添加技能类型配置
在技能装配这里,我们分了主动技能和被动技能,不同类型的技能无法装配。所以,我们需要在技能数据里设置对应的类型,我们还是使用类型标签设置
我们在技能数据结构体内增加一个配置项,用于设置类型
然后在标签管理这里,增加三项,主动技能,被动技能和空
cpp
FGameplayTag Abilities_Type_Offensive; //技能类型 主动技能
FGameplayTag Abilities_Type_Passive; //技能类型 被动技能
FGameplayTag Abilities_Type_None; //技能类型 空 受击等技能设置
并且注册到标签管理器
cpp
/*
* 当前技能类型标签
*/
GameplayTags.Abilities_Type_Offensive = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Abilities.Type.Offensive"),
FString("主动技能")
);
GameplayTags.Abilities_Type_Passive = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Abilities.Type.Passive"),
FString("被动技能")
);
GameplayTags.Abilities_Type_None = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Abilities.Type.None"),
FString("啥也不是")
);
接着打开UE,在技能配置里,设置技能的对应类型
添加技能装配事件触发
我们也注意到,技能分为两大类型,主动技能和被动技能,为了区分它们,我们需要设置对应的类型标签。
首先在装配按钮这里添加可设置的类型变量,注意把眼睛打开。
然后在外部设置它对应的类型
被动技能也设置
然后给按钮绑定一个点击事件
添加技能装配处理逻辑
我们需要在技能面板控制器里增加一个点击按钮,用来实现点击事件
cpp
UFUNCTION(BlueprintCallable)
void EquipButtonPressed(const FGameplayTag& SlotTag, const FGameplayTag& AbilityType); //装配技能按钮按下事件
在实现这里,我首先判断条件是否达成,然后调用ASC里的实际处理技能装配的逻辑
cpp
void USpellMenuWidgetController::EquipButtonPressed(const FGameplayTag& SlotTag, const FGameplayTag& AbilityType)
{
const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();
//获取装配技能的类型
const FGameplayTag& SelectedAbilityType = AbilityInfo->FindAbilityInfoForTag(SelectedAbility.Ability).AbilityType;
if(!SelectedAbilityType.MatchesTagExact(AbilityType)) return; //类型不同无法装配
//获取装配技能的输入标签
const FGameplayTag& SelectedAbilityInputTag = GetRPGASC()->GetInputTagFromAbilityTag(SelectedAbility.Ability);
if(SelectedAbilityInputTag.MatchesTagExact(SlotTag)) return; //如果当前技能输入和插槽标签相同,证明已经装配,不需要再处理
//调用装配技能函数,进行处理
GetRPGASC()->ServerEquipAbility(SelectedAbility.Ability, SlotTag);
}
接着,我们在ASC里增加多个函数,用于实现这个逻辑,为什么在ASC里,因为GA是属于GAS系统的,GAS相关的内容就放在GAS相关的类里实现处理
cpp
UFUNCTION(Server, Reliable) //在服务器处理技能装配,传入技能标签和装配的技能标签
void ServerEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Slot);
UFUNCTION(Client, Reliable) //在客户端处理技能装配
void ClientEquipAbility(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PreviousSlot);
void ClearSlot(FGameplayAbilitySpec* Spec); //清除技能装配插槽的技能
void ClearAbilitiesOfSlot(const FGameplayTag& Slot); //根据输入标签,清除技能装配插槽的技能
static bool AbilityHasSlot(FGameplayAbilitySpec* Spec, const FGameplayTag& Slot); //判断当前技能实例是否处于目标技能装配插槽
首先,我们看一下后面三个函数,他们是为了处理技能实现的函数。
首先是AbilityHasSlot,需要传入一个技能实例和一个输入标签(插槽标识),用来判断技能是否属于这个插槽
cpp
bool URPGAbilitySystemComponent::AbilityHasSlot(FGameplayAbilitySpec* Spec, const FGameplayTag& Slot)
{
for(FGameplayTag Tag : Spec->DynamicAbilityTags)
{
if(Tag.MatchesTagExact(Slot))
{
return true;
}
}
return false;
}
然后就是清除掉技能的装配的插槽,其实就是清除掉GA的输入标签
cpp
void URPGAbilitySystemComponent::ClearSlot(FGameplayAbilitySpec* Spec)
{
const FGameplayTag Slot = GetInputTagFromSpec(*Spec);
Spec->DynamicAbilityTags.RemoveTag(Slot);
MarkAbilitySpecDirty(*Spec);
}
然后就是根据输入标签(插槽)清除掉所有技能的对应的插槽,这个会用到上面的两个函数。
cpp
void URPGAbilitySystemComponent::ClearAbilitiesOfSlot(const FGameplayTag& Slot)
{
FScopedAbilityListLock ActiveScopeLock(*this);
for(FGameplayAbilitySpec& Spec : GetActivatableAbilities())
{
if(AbilityHasSlot(&Spec, Slot))
{
ClearSlot(&Spec);
}
}
}
接下来就是装配函数,我们先获取到需要装配的技能实例,获取到当前装配的插槽和当前的技能的状态标签,然后将需要装配到的目标插槽的的技能清除掉,并将技能自身的插槽清除,并将对应的标签修改掉。然后触发客户端的调用,并及时将技能的修改复制到客户端(当前执行只在服务器运行,客户端不会运行,只需要将结果复制到即可)
cpp
void URPGAbilitySystemComponent::ServerEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Slot)
{
if(FGameplayAbilitySpec* AbilitySpec = GetSpecFromAbilityTag(AbilityTag))
{
const FGameplayTag& PrevSlot = GetInputTagFromSpec(*AbilitySpec); //技能之前装配的插槽
const FGameplayTag& Status = GetStatusTagFromSpec(*AbilitySpec); //当前技能的状态标签
//判断技能的状态,技能状态只有在已装配或者已解锁的状态才可以装配
const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();
if(Status == GameplayTags.Abilities_Status_Equipped || Status == GameplayTags.Abilities_Status_Unlocked)
{
ClearAbilitiesOfSlot(Slot); //通过技能的输入标签清除掉插槽的技能
ClearSlot(AbilitySpec); //清除掉当前技能的输入标签
AbilitySpec->DynamicAbilityTags.AddTag(Slot); //将目标插槽的输入标签添加到技能实例的动态标签容器中
//如果状态标签是已解锁,我们需要将其修改为已装配状态
if(Status.MatchesTagExact(GameplayTags.Abilities_Status_Unlocked))
{
AbilitySpec->DynamicAbilityTags.RemoveTag(GameplayTags.Abilities_Status_Unlocked);
AbilitySpec->DynamicAbilityTags.AddTag(GameplayTags.Abilities_Status_Equipped);
}
ClientEquipAbility(AbilityTag, Status, Slot, PrevSlot);
MarkAbilitySpecDirty(*AbilitySpec); //立即将其复制到每个客户端
}
}
}
在客户端,我们只进行一个委托的广播,然后让控制器监听去修改
所以,我们增加一个技能装配后的委托
cpp
DECLARE_MULTICAST_DELEGATE_FourParams(FAbilityEquipped, const FGameplayTag& /*技能标签*/, const FGameplayTag& /*技能状态标签*/, const FGameplayTag& /*输入标签*/, const FGameplayTag& /*上一个输入标签*/);
cpp
FAbilityEquipped AbilityEquipped; //技能装配更新回调
然后在客户端执行的函数里进行调用
cpp
void URPGAbilitySystemComponent::ClientEquipAbility_Implementation(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PreviousSlot)
{
AbilityEquipped.Broadcast(AbilityTag, Status, Slot, PreviousSlot); //在客户端将更新后的标签广播
}
在控制器接收技能装配委托
接下来我们要在控制器实现对技能装配委托的监听,考虑到,技能装配后,在技能面板和Overlay里都需要使用它,我们将函数写到基类里,然后在对应的控制器里进行监听绑定。
我们在控制器基类增加一个委托回调函数
cpp
//监听技能装配后的处理
void OnAbilityEquipped(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PreviousSlot) const;
然后,在实现里,我们通过传递过来的标签,实现技能数据的广播,然后在UI监听更新
cpp
void URPGWidgetController::OnAbilityEquipped(const FGameplayTag& AbilityTag, const FGameplayTag& Status, const FGameplayTag& Slot, const FGameplayTag& PreviousSlot) const
{
const FRPGGameplayTags GameplayTags = FRPGGameplayTags::Get();
//清除旧插槽的数据
FRPGAbilityInfo LastSlotInfo;
LastSlotInfo.StatusTag = GameplayTags.Abilities_Status_Unlocked;
LastSlotInfo.InputTag = PreviousSlot;
LastSlotInfo.AbilityTag = GameplayTags.Abilities_None;
AbilityInfoDelegate.Broadcast(LastSlotInfo);
//更新新插槽的数据
FRPGAbilityInfo Info = AbilityInfo->FindAbilityInfoForTag(AbilityTag);
Info.StatusTag = Status;
Info.InputTag = Slot;
AbilityInfoDelegate.Broadcast(Info);
}
我们在需要监听的派生类里,添加对其的监听
cpp
//监听技能装配的回调
GetRPGASC()->AbilityEquipped.AddUObject(this, &USpellMenuWidgetController::OnAbilityEquipped);
编译打开代码,我们在数据接收这里,增加,在判断是否为对应插槽的更新数据,如果技能标签为空,则是清除旧插槽。
在技能按钮里,还需要多做一步操作就是处理技能冷却的监听处理
解决降级为0级技能还装配的问题
当技能等级降级为0级以后,技能的装配变为了无法装配,我们需要将此信息同步到技能栏和装配技能栏。
所以我们在降级函数中,将其增加一个委托,再调用客户端更新
接下来是效果展示