GameplayAbilitySystem入门与实战(三):几个有用的异步事件

前言

这一篇单独创建几个非常有用的异步节点,如UAsyncTask_ListenAttributeChanged可以实时监听任意属性的更改情况,在初期我们用于临时UMG中,方便我们查询和debug;

可以使用继承自UBlueprintAsyncActionBase的基本异步类, 也可以使用GAS框架内的UAbilityTask来制作

image-20201214155057270

异步事件:属性监听

这个节点相对比较简单, 原理就是通过ASC中的GetGameplayAttributeValueChangeDelegate()绑定对应属性的代理,然后简单的派发异步节点中的代理, 这是GASD项目采用的方案,

  • 头文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnAttributeChanged, FGameplayAttribute, Attribute, float, NewValue, float, OldValue);

UCLASS()
class SUPERROAD_API UAsyncTask_ListenAttributeChanged : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable)
FOnAttributeChanged OnAttributeChanged;

// 监听attribute 改变
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTask_ListenAttributeChanged* ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute);

// 数组版本
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTask_ListenAttributeChanged* ListenForAttributesChange(UAbilitySystemComponent* AbilitySystemComponent, TArray<FGameplayAttribute> Attributes);

UFUNCTION(BlueprintCallable)
void EndTask();

protected:
UPROPERTY()
UAbilitySystemComponent* ASC;

FGameplayAttribute AttributeToListenFor;
TArray<FGameplayAttribute> AttributesToListenFor;

void AttributeChanged(const FOnAttributeChangeData& Data);
};

没什么特殊需要注意的, 提供了2个版本的监听

  • cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UAsyncTask_ListenAttributeChanged* UAsyncTask_ListenAttributeChanged::ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute)
{
UAsyncTask_ListenAttributeChanged* WaitForAttributeChangedTask = NewObject<UAsyncTask_ListenAttributeChanged>();
WaitForAttributeChangedTask->ASC = AbilitySystemComponent;
WaitForAttributeChangedTask->AttributeToListenFor = Attribute;

if (!IsValid(AbilitySystemComponent) || !Attribute.IsValid())
{
WaitForAttributeChangedTask->RemoveFromRoot();
return nullptr;
}

AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(WaitForAttributeChangedTask, &UAsyncTask_ListenAttributeChanged::AttributeChanged);

return WaitForAttributeChangedTask;
}

构造一个异步节点类, 赋值,绑定ASC事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UAsyncTask_ListenAttributeChanged* UAsyncTask_ListenAttributeChanged::ListenForAttributesChange(UAbilitySystemComponent* AbilitySystemComponent, TArray<FGameplayAttribute> Attributes)
{
UAsyncTask_ListenAttributeChanged* WaitForAttributeChangedTask = NewObject<UAsyncTask_ListenAttributeChanged>();
WaitForAttributeChangedTask->ASC = AbilitySystemComponent;
WaitForAttributeChangedTask->AttributesToListenFor = Attributes;

if (!IsValid(AbilitySystemComponent) || Attributes.Num() < 1)
{
WaitForAttributeChangedTask->RemoveFromRoot();
return nullptr;
}

for (FGameplayAttribute Attribute : Attributes)
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(WaitForAttributeChangedTask, &UAsyncTask_ListenAttributeChanged::AttributeChanged);
}

return WaitForAttributeChangedTask;
}

数组版本,区别不大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void UAsyncTask_ListenAttributeChanged::EndTask()
{
if (IsValid(ASC))
{
ASC->GetGameplayAttributeValueChangeDelegate(AttributeToListenFor).RemoveAll(this);

for (FGameplayAttribute Attribute : AttributesToListenFor)
{
ASC->GetGameplayAttributeValueChangeDelegate(Attribute).RemoveAll(this);
}
}

SetReadyToDestroy();
MarkPendingKill();
}

EndTask事件不能忘记执行,否则可能会导致引擎崩溃

在这里就是解除绑定

1
2
3
4
void UAsyncTask_ListenAttributeChanged::AttributeChanged(const FOnAttributeChangeData& Data)
{
OnAttributeChanged.Broadcast(Data.Attribute, Data.NewValue, Data.OldValue);
}

转发,派发代理


这里创建了一个UMG控件通过公开的属性变量来监听特定的属性

image-20201214155732889

然后就放到一个DebugUI中,监听所有属性

image-20201214155800692

这是运行以后的

image-20201214155816195


如果你想要自己定义一套属性响应机制也可以通过比如在AttributeSet内广播代理事件来达到同样的目的, 我们模仿ASC的那一套来实现一下

1
2
3
4
5
6
DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnAttributeChangedMulDly, const FGameplayAttribute&,float,float);
public:
FOnAttributeChangedMulDly& GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute);
protected:
void BroadcastValueChanged(FGameplayAttribute Attribute);
TMap<FGameplayAttribute, FOnAttributeChangedMulDly> AttributeValueChangeDelegates;

然后异步事件节点通过类似的方式绑定就可以了, 我们这里输出的是CurrentBase值, 与前面方案有所不同

异步事件:冷却

冷却时间比前面的属性略微复杂一点点, 监听了两个时间,开始和结束,并没有中间过程, 查看ASC以及GEGA的代码发现并无相关的事件代理, 但是可以有方法找到当前技能或者GERemainingTimeDuration.

无妨, 这个或许是因为消耗问题, 没有必要一直派发代理来告诉我们CD刷新了, 我们做测试或者到时候UI显示的转圈圈效果也只需要做个本地的效果就可以了

  • 头文件
1
2
3
4
5
6
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnCooldownChanged, FGameplayTag, CooldownTag, float, TimeRemaining, float, Duration);

UPROPERTY(BlueprintAssignable)
FOnCooldownChanged OnCooldownBegin;
UPROPERTY(BlueprintAssignable)
FOnCooldownChanged OnCooldownEnd;

类似的创建静态事件返回自己

1
2
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTask_ListenCooldownUpdated* ListenForCooldownChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayTagContainer InCooldownTags, bool InUseServerCooldown);

几个Protected

1
2
3
4
5
6
7
8
protected:
UPROPERTY()
UAbilitySystemComponent* ASC;
FGameplayTagContainer CooldownTags;
bool UseServerCooldown;
virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
virtual void CooldownTagChanged(const FGameplayTag CooldownTag, int32 NewCount);
bool GetCooldownRemainingForTag(FGameplayTagContainer InCooldownTags, float& TimeRemaining, float& CooldownDuration)
  • cpp

先看激活GE的回调事件

先从Spec中拿到需要的tag,查询我们指定的冷却tag是否包含在里面

如果存在, 那么拿到所需要的时间根据相关设置广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void UAsyncTask_ListenCooldownUpdated::OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle)
{
FGameplayTagContainer AssetTags;
SpecApplied.GetAllAssetTags(AssetTags);

FGameplayTagContainer GrantedTags;
SpecApplied.GetAllGrantedTags(GrantedTags);

TArray<FGameplayTag> CooldownTagArray;
CooldownTags.GetGameplayTagArray(CooldownTagArray);

for (FGameplayTag CooldownTag : CooldownTagArray)
{
if (AssetTags.HasTagExact(CooldownTag) || GrantedTags.HasTagExact(CooldownTag))
{
float TimeRemaining = 0.0f;
float Duration = 0.0f;

FGameplayTagContainer CooldownTagContainer(GrantedTags.GetByIndex(0));
GetCooldownRemainingForTag(CooldownTagContainer, TimeRemaining, Duration);

if (ASC->GetOwnerRole() == ROLE_Authority)
{
// 服务端玩家
OnCooldownBegin.Broadcast(CooldownTag, TimeRemaining, Duration);
}
else if (!UseServerCooldown && SpecApplied.GetContext().GetAbilityInstance_NotReplicated())
{
// 客户端使用预测冷却时间
OnCooldownBegin.Broadcast(CooldownTag, TimeRemaining, Duration);
}
else if (UseServerCooldown && SpecApplied.GetContext().GetAbilityInstance_NotReplicated() == nullptr)
{
// 客户端使用服务端冷却
OnCooldownBegin.Broadcast(CooldownTag, TimeRemaining, Duration);
}
else if (UseServerCooldown && SpecApplied.GetContext().GetAbilityInstance_NotReplicated())
{
//客户端使用服务器的冷却时间,但这是GE预测的冷却时间。
//在服务器冷却时间到来之前,这可以使技能变灰。
OnCooldownBegin.Broadcast(CooldownTag, -1.0f, -1.0f);
}
}
}
}

通过tag找到所需的时间, 这两个时间是保存在GE中的, 也可以通过ASC去查询得到.

下面方法是从数组中找到值最大的作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bool UAsyncTask_ListenCooldownUpdated::GetCooldownRemainingForTag(FGameplayTagContainer InCooldownTags, float& TimeRemaining, float& CooldownDuration)
{
if (IsValid(ASC) && InCooldownTags.Num() > 0)
{
TimeRemaining = 0.f;
CooldownDuration = 0.f;

FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(InCooldownTags);
TArray< TPair<float, float> > DurationAndTimeRemaining = ASC->GetActiveEffectsTimeRemainingAndDuration(Query);
if (DurationAndTimeRemaining.Num() > 0)
{
int32 BestIdx = 0;
float LongestTime = DurationAndTimeRemaining[0].Key;
for (int32 Idx = 1; Idx < DurationAndTimeRemaining.Num(); ++Idx)
{
if (DurationAndTimeRemaining[Idx].Key > LongestTime)
{
LongestTime = DurationAndTimeRemaining[Idx].Key;
BestIdx = Idx;
}
}

TimeRemaining = DurationAndTimeRemaining[BestIdx].Key;
CooldownDuration = DurationAndTimeRemaining[BestIdx].Value;

return true;
}
}

return false;
}

下面再看静态方法, 这个也没什么大的难点, 绑定了GE激活的代理, 然后根据tag注册tag对应的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

UAsyncTask_ListenCooldownUpdated* UAsyncTask_ListenCooldownUpdated::ListenForCooldownChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayTagContainer InCooldownTags, bool InUseServerCooldown)
{
UAsyncTask_ListenCooldownUpdated* task = NewObject<UAsyncTask_ListenCooldownUpdated>();
task->ASC = AbilitySystemComponent;
task->CooldownTags = InCooldownTags;
task->UseServerCooldown = InUseServerCooldown;

if (!IsValid(AbilitySystemComponent) || InCooldownTags.Num() < 1)
{
task->EndTask();
return nullptr;
}

AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(task, &UAsyncTask_ListenCooldownUpdated::OnActiveGameplayEffectAddedCallback);

TArray<FGameplayTag> CooldownTagArray;
InCooldownTags.GetGameplayTagArray(CooldownTagArray);

for (FGameplayTag CooldownTag : CooldownTagArray)
{
AbilitySystemComponent->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved).AddUObject(task, &UAsyncTask_ListenCooldownUpdated::CooldownTagChanged);
}

return task;
}

结束事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void UAsyncTask_ListenCooldownUpdated::EndTask()
{
if (IsValid(ASC))
{
ASC->OnActiveGameplayEffectAddedDelegateToSelf.RemoveAll(this);

TArray<FGameplayTag> CooldownTagArray;
CooldownTags.GetGameplayTagArray(CooldownTagArray);

for (FGameplayTag CooldownTag : CooldownTagArray)
{
ASC->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved).RemoveAll(this);
}
}

SetReadyToDestroy();
MarkPendingKill();
}

最后是tag改变的事件

1
2
3
4
5
6
7
void UAsyncTask_ListenCooldownUpdated::CooldownTagChanged(const FGameplayTag CooldownTag, int32 NewCount)
{
if (NewCount == 0)
{
OnCooldownEnd.Broadcast(CooldownTag, -1.0f, -1.0f);
}
}

异步事件:GE层数

这个节点用于监听可以叠加的GE的层数

  • 头文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnGameplayEffectStackChanged, FGameplayTag, EffectGameplayTag, FActiveGameplayEffectHandle, Handle, int32, NewStackCount, int32, OldStackCount);

UCLASS()
class SUPERROAD_API UAsyncTask_ListenGEStackChanged : public UBlueprintAsyncActionBase
{
GENERATED_BODY()


UPROPERTY(BlueprintAssignable)
FOnGameplayEffectStackChanged OnGameplayEffectStackChange;

UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTask_ListenGEStackChanged* ListenForGameplayEffectStackChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayTag InEffectGameplayTag);

UFUNCTION(BlueprintCallable)
void EndTask();

protected:
UPROPERTY()
UAbilitySystemComponent* ASC;

FGameplayTag EffectGameplayTag;

virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

virtual void GameplayEffectStackChanged(FActiveGameplayEffectHandle EffectHandle, int32 NewStackCount, int32 PreviousStackCount);
};

头文件跟之前的区别不大, 三个本地事件分别用于绑定 激活/移除GE和层数变化

  • cpp

静态函数绑定了2个代理

OnActiveGameplayEffectAddedDelegateToSelfOnAnyGameplayEffectRemovedDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UAsyncTask_ListenGEStackChanged* UAsyncTask_ListenGEStackChanged::ListenForGameplayEffectStackChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayTag InEffectGameplayTag)
{

UAsyncTask_ListenGEStackChanged* task = NewObject<UAsyncTask_ListenGEStackChanged>();
task->ASC = AbilitySystemComponent;
task->EffectGameplayTag = InEffectGameplayTag;

if (!IsValid(AbilitySystemComponent) || !InEffectGameplayTag.IsValid())
{
task->EndTask();
return nullptr;
}

AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(task, &UAsyncTask_ListenGEStackChanged::OnActiveGameplayEffectAddedCallback);
AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(task, &UAsyncTask_ListenGEStackChanged::OnRemoveGameplayEffectCallback);

return task;
}

然后结束任务的时候接触绑定

1
2
3
4
5
6
7
8
9
10
11
void UAsyncTask_ListenGEStackChanged::EndTask()
{
if (IsValid(ASC))
{
ASC->OnActiveGameplayEffectAddedDelegateToSelf.RemoveAll(this);
ASC->OnAnyGameplayEffectRemovedDelegate().RemoveAll(this);
}

SetReadyToDestroy();
MarkPendingKill();
}

激活GE的时候做了一个tag判断,如果复合就绑定OnGameplayEffectStackChangeDelegate并且广播代理

同理在移除的时候也广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void UAsyncTask_ListenGEStackChanged::OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle)
{
FGameplayTagContainer AssetTags;
SpecApplied.GetAllAssetTags(AssetTags);

FGameplayTagContainer GrantedTags;
SpecApplied.GetAllGrantedTags(GrantedTags);

if (AssetTags.HasTagExact(EffectGameplayTag) || GrantedTags.HasTagExact(EffectGameplayTag))
{
ASC->OnGameplayEffectStackChangeDelegate(ActiveHandle)->AddUObject(this, &UAsyncTask_ListenGEStackChanged::GameplayEffectStackChanged);
OnGameplayEffectStackChange.Broadcast(EffectGameplayTag, ActiveHandle, 1, 0);
}
}

void UAsyncTask_ListenGEStackChanged::OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved)
{
FGameplayTagContainer AssetTags;
EffectRemoved.Spec.GetAllAssetTags(AssetTags);

FGameplayTagContainer GrantedTags;
EffectRemoved.Spec.GetAllGrantedTags(GrantedTags);

if (AssetTags.HasTagExact(EffectGameplayTag) || GrantedTags.HasTagExact(EffectGameplayTag))
{
OnGameplayEffectStackChange.Broadcast(EffectGameplayTag, EffectRemoved.Handle, 0, 1);
}
}

void UAsyncTask_ListenGEStackChanged::GameplayEffectStackChanged(FActiveGameplayEffectHandle EffectHandle, int32 NewStackCount, int32 PreviousStackCount)
{
OnGameplayEffectStackChange.Broadcast(EffectGameplayTag, EffectHandle, NewStackCount, PreviousStackCount);
}