在上一篇文章里,我们实现了通过GE给技能增加资源消耗和技能冷却功能。UI也能够显示角色能够使用的技能的UI,现在还有一个问题,我们希望在技能释放进去冷却时,技能变成灰色,并在技能冷却完成,技能可以再次使用。
为了实现这个功能,我们首先要实现一个能够监听技能进入冷却的方法,然后在技能被使用后,将UI的颜色修改,并在技能冷却完成后,将技能UI恢复到可释放状态。
创建异步任务来监听技能冷却
为了实现能够监听,我们创建一个新的类用来监听技能冷却。
我们创建一个新的c++类,继承至BlueprintAsyncActionBase类
修改命名,我们将其命名为监听冷却修改
接下来,我们将实现类,如果不想看实现过程,请略过,实现完成,我会贴上完整的实现代码。
首先我们创建一个委托宏,用于设置委托类型,返回一个参数
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCooldownChangeSignature, float, TimeRemaining);
在类里创建两个委托,用于触发在技能进入冷却时触发,然后在技能冷却时触发
UPROPERTY(BlueprintAssignable) FCooldownChangeSignature CooldownStart; //冷却触发开始委托 UPROPERTY(BlueprintAssignable) FCooldownChangeSignature CooldownEnd; //冷却结束委托
然后创建两个保护性的变量参数,用于实例类时,存储ASC和需要监听的冷却标签
protected: UPROPERTY() TObjectPtr<UAbilitySystemComponent> ASC; FGameplayTag CooldownTag; //记录监听的冷却标签
接着我们创建一个实例函数,用于创建类的实例设置meta=(BlueprintInternalUseOnly=“true”)为了防止开发mod或者玩家能够使用到此函数。
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly="true")) static UListenCooldownChange* ListenForCooldownChange(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayTag& CooldownTag);
然后增加一个函数,用于结束任务,防止内存泄露
UFUNCTION(BlueprintCallable) void EndTask();
在创建实例函数中,我们首先实例化类,并将参数设置上去
UListenCooldownChange* UListenCooldownChange::ListenForCooldownChange(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayTag& InCooldownTag) { UListenCooldownChange* ListenCooldownChange = NewObject<UListenCooldownChange>(); ListenCooldownChange->ASC = AbilitySystemComponent; ListenCooldownChange->CooldownTag = InCooldownTag;
然后判断传入的两个参数是否存在,如果未存在,将结束此任务
//如果参数有一项未设置,我们将结束此任务 if(!IsValid(AbilitySystemComponent) || !InCooldownTag.IsValid()) { ListenCooldownChange->EndTask(); return nullptr; }
我们接下来增加两个回调函数,用于实现对技能冷却的开始和结束的广播
//监听冷却标签回调函数 void CooldownTagChanged(const FGameplayTag InCooldownTag, int32 NewCount); //监听ASC激活GE的回调 void OnActiveEffectAdded(UAbilitySystemComponent* TargetASC, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveEffectHandle);
使用对标签的监听来实现技能冷却结束的监听
//监听冷却标签的变动,并绑定回调,用于获取冷却结束 AbilitySystemComponent->RegisterGameplayTagEvent(InCooldownTag, EGameplayTagEventType::NewOrRemoved) .AddUObject(ListenCooldownChange, &UListenCooldownChange::CooldownTagChanged);
对于技能进入冷却状态,我们采用监听应用冷却GE实现
//监听GE应用回调,获取冷却激活,用于获取技能开始进入冷却 AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(ListenCooldownChange, &UListenCooldownChange::OnActiveEffectAdded);
对于冷却的开始的参数设置,我们可以查看ASC源码,这个可以在客户端和服务器都获取到对应的委托回调来监听有时效性的GE
宏的定义时,是传入了三个参数
设置完成后,我们就可以返回,每次调用,我们可以创建一个监听实例
return ListenCooldownChange;
为了防止内存泄露,我们需要实现EndTask函数,在实例不需要时,对其进行销毁,并进行资源回收。AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf是由ASC创建的,所以不需要我们去对其进行销毁
void UListenCooldownChange::EndTask() { //判断ASC是否存在 if(!IsValid(ASC)) return; //取消对冷却标签的变动监听 ASC->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved).RemoveAll(this); SetReadyToDestroy(); //设置此对象可以被销毁,如果此对象不再被引用,将可以被销毁掉 MarkAsGarbage(); //标记此实例为垃圾资源,可以被回收 }
接着,我们还需要实现两个广播的处理,首先是对冷却结束的广播,我们对冷却标签进行获取,如果标签数量小于1,那么,技能将不处于冷却状态,我们广播冷却结束即可
void UListenCooldownChange::CooldownTagChanged(const FGameplayTag InCooldownTag, int32 NewCount) const { //如果计数为0,代表冷却标签已经不存在,技能不处于冷却状态 if(NewCount == 0) { //广播冷却结束委托 CooldownEnd.Broadcast(0.f); } }
然后就是进入冷却的广播函数广播,我们首先获取这个应用的GE是否为设置了冷却标签,为了防止设置错误,我们获取了设置自身和设置给Actor的标签容器,判断容器内是否拥有我们设置的冷却标签。然后创建查询冷却标签的查询器对象,通过此对象去查找剩余的冷却时间,从中获取到最大冷却时间将时间广播出去。
void UListenCooldownChange::OnActiveEffectAdded(UAbilitySystemComponent* TargetASC, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveEffectHandle) const { //获取设置到自身的所有标签 FGameplayTagContainer AssetTags; SpecApplied.GetAllAssetTags(AssetTags); //获取到GE设置给Actor的标签 FGameplayTagContainer GrantedTags; SpecApplied.GetAllGrantedTags(GrantedTags); //判断应用的GE是否设置了此冷却标签 if(AssetTags.HasTagExact(CooldownTag) || GrantedTags.HasTagExact(CooldownTag)) { //创建一个查询对象,用于查询包含所有标签容器标签的GE FGameplayEffectQuery GameplayEffectQuery = FGameplayEffectQuery::MakeQuery_MatchAllOwningTags(CooldownTag.GetSingleTagContainer()); //返回查询到的所有包含此冷却GE的剩余时间的GE TArray<float> TimesRemaining = ASC->GetActiveEffectsTimeRemaining(GameplayEffectQuery); if(TimesRemaining.Num() > 0) { //获取最高的冷却时间 float TimeRemaining = TimesRemaining[0]; for(int32 i=0; i<TimesRemaining.Num(); i++) { if(TimeRemaining < TimesRemaining[i]) TimeRemaining = TimesRemaining[i]; } //广播初始时间 CooldownStart.Broadcast(TimeRemaining); } } }
接下来就是完整代码,不想看代码解析的,自己复制代码去修改名称运行即可
ListenCooldownChange.h
// 版权归暮志未晚所有。 #pragma once #include "CoreMinimal.h" #include "GameplayTagContainer.h" #include "AbilitySystemComponent.h" #include "Kismet/BlueprintAsyncActionBase.h" #include "ListenCooldownChange.generated.h" struct FGameplayAbilitySpec; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCooldownChangeSignature, float, TimeRemaining); /** * */ UCLASS(BlueprintType, meta = (ExposedAsyncProxy="AsyncTask")) class RPG_API UListenCooldownChange : public UBlueprintAsyncActionBase { GENERATED_BODY() public: UPROPERTY(BlueprintAssignable) FCooldownChangeSignature CooldownStart; //冷却触发开始委托 UPROPERTY(BlueprintAssignable) FCooldownChangeSignature CooldownEnd; //冷却结束委托 UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly="true")) static UListenCooldownChange* ListenForCooldownChange(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayTag& InCooldownTag); UFUNCTION(BlueprintCallable) void EndTask(); protected: UPROPERTY() TObjectPtr<UAbilitySystemComponent> ASC; FGameplayTag CooldownTag; //记录监听的冷却标签 //监听冷却标签回调函数 void CooldownTagChanged(const FGameplayTag InCooldownTag, int32 NewCount) const; //监听ASC激活GE的回调 void OnActiveEffectAdded(UAbilitySystemComponent* TargetASC, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveEffectHandle) const; };
ListenCooldownChange.cpp
// 版权归暮志未晚所有。 #include "AbilitySystem/AsyncTasks/ListenCooldownChange.h" #include "AbilitySystemComponent.h" UListenCooldownChange* UListenCooldownChange::ListenForCooldownChange(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayTag& InCooldownTag) { UListenCooldownChange* ListenCooldownChange = NewObject<UListenCooldownChange>(); ListenCooldownChange->ASC = AbilitySystemComponent; ListenCooldownChange->CooldownTag = InCooldownTag; //如果参数有一项未设置,我们将结束此任务 if(!IsValid(AbilitySystemComponent) || !InCooldownTag.IsValid()) { ListenCooldownChange->EndTask(); return nullptr; } //监听冷却标签的变动,并绑定回调,用于获取冷却结束 AbilitySystemComponent->RegisterGameplayTagEvent(InCooldownTag, EGameplayTagEventType::NewOrRemoved) .AddUObject(ListenCooldownChange, &UListenCooldownChange::CooldownTagChanged); //监听GE应用回调,获取冷却激活,用于获取技能开始进入冷却 AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(ListenCooldownChange, &UListenCooldownChange::OnActiveEffectAdded); return ListenCooldownChange; } void UListenCooldownChange::EndTask() { //判断ASC是否存在 if(!IsValid(ASC)) return; //取消对冷却标签的变动监听 ASC->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved).RemoveAll(this); SetReadyToDestroy(); //设置此对象可以被销毁,如果此对象不再被引用,将可以被销毁掉 MarkAsGarbage(); //标记此实例为垃圾资源,可以被回收 } void UListenCooldownChange::CooldownTagChanged(const FGameplayTag InCooldownTag, int32 NewCount) const { //如果计数为0,代表冷却标签已经不存在,技能不处于冷却状态 if(NewCount == 0) { //广播冷却结束委托 CooldownEnd.Broadcast(0.f); } } void UListenCooldownChange::OnActiveEffectAdded(UAbilitySystemComponent* TargetASC, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveEffectHandle) const { //获取设置到自身的所有标签 FGameplayTagContainer AssetTags; SpecApplied.GetAllAssetTags(AssetTags); //获取到GE设置给Actor的标签 FGameplayTagContainer GrantedTags; SpecApplied.GetAllGrantedTags(GrantedTags); //判断应用的GE是否设置了此冷却标签 if(AssetTags.HasTagExact(CooldownTag) || GrantedTags.HasTagExact(CooldownTag)) { //创建一个查询对象,用于查询包含所有标签容器标签的GE FGameplayEffectQuery GameplayEffectQuery = FGameplayEffectQuery::MakeQuery_MatchAllOwningTags(CooldownTag.GetSingleTagContainer()); //返回查询到的所有包含此冷却GE的剩余时间的GE TArray<float> TimesRemaining = ASC->GetActiveEffectsTimeRemaining(GameplayEffectQuery); if(TimesRemaining.Num() > 0) { //获取最高的冷却时间 float TimeRemaining = TimesRemaining[0]; for(int32 i=0; i<TimesRemaining.Num(); i++) { if(TimeRemaining < TimesRemaining[i]) TimeRemaining = TimesRemaining[i]; } //广播初始时间 CooldownStart.Broadcast(TimeRemaining); } } }
接着,我们在UI的事件图标中搜索名称,查看是否能够找到对应的节点
注意,我们搜索的名称是函数名称
测试代码
上面我们编写了对应的代码,首先做的就是快速测试一下,防止出现问题。
我们快速连一套测试节点,用来检测是否能够获取到对应事件
查看打印,发现事件确实成功触发,也有一些问题,比如触发了多次。触发多次的原因是因为所有的技能UI都是在监听这一个冷却标签
处理无法在蓝图调用的问题
我们当前无法在蓝图中获取对象进行调用销毁事件,所以需要一个方法,获取对象,我们在头文件设置,将其作为一个可获取参数,并设置命名"AsyncTask"
UCLASS(BlueprintType, meta = (ExposedAsyncProxy="AsyncTask"))
编译运行,可以查看到对象类型
我们可以将其设置为变量,避免没有销毁掉
设置冷却标签
我们需要记录技能的冷却标签,有一个比较好的方法就是在技能数据结构体增加一个配置项
在数据资产中配置上
接着我们修改ui的蓝图,在应用了技能数据后,对其绑定技能的回调,为了保证内存泄露,现进行销毁,防止频繁切换ui显示的技能导致频繁触发回调。
在ui被销毁时,也需要调用
接着编写代码测试
经过测试,发现还是有问题,原来是没有判断是否为当前需要监听的技能
和之前的技能一样,我们通过标签判断是否需要执行后续
这样就实现了事件监听
创建UI冷却效果
上面,我们实现了技能的冷却进入和退出。
我们接下来,要将冷却效果表现到UI上面,让玩家能够清晰的了解到技能已经进入冷却,无法释放。
我们增加两个函数节点,一个是设置技能UI变暗,并将冷却时间显示出来
另一个则是恢复默认状态,将冷却节点隐藏,并将技能图标颜色恢复默认
我们在监听到技能进入冷却后,将冷却时间保存为变量,方便后续使用,并进入冷却状态
然后我们设置一个定时器,在定时器里面对显示剩余时间进行更新,Time为多次时间更新一次,Looping选中,定时器将循环更新,不勾选将只触发一次。
并将定时器的引用保存下来,方便在冷却结束后,将其销毁
然后在自定义事件里面,减去每次调用时间,更新冷却时间,并显示到UI上面
为了防止出现负数,我们将其限制在最小值为0
在技能冷却结束后,我们将定时器清除,并恢复默认状态
接下来运行查看效果
在技能冷却完成,也能恢复默认
接下来,截取一张完整的蓝图