Published on

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

Authors
  • avatar
    Name
    东哥
    Twitter

前言

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

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

image-20201214155057270

异步事件:属性监听

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

  • 头文件
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
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事件

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;
}

数组版本,区别不大

void UAsyncTask_ListenAttributeChanged::EndTask()
{
	if (IsValid(ASC))
	{
		ASC->GetGameplayAttributeValueChangeDelegate(AttributeToListenFor).RemoveAll(this);

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

	SetReadyToDestroy();
	MarkPendingKill();
}

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

在这里就是解除绑定

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的那一套来实现一下

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显示的转圈圈效果也只需要做个本地的效果就可以了

  • 头文件
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnCooldownChanged, FGameplayTag, CooldownTag, float, TimeRemaining, float, Duration);

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

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

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

几个Protected

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是否包含在里面

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

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去查询得到.

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

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对应的事件


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;
}

结束事件

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改变的事件

void UAsyncTask_ListenCooldownUpdated::CooldownTagChanged(const FGameplayTag CooldownTag, int32 NewCount)
{
	if (NewCount == 0)
	{
		OnCooldownEnd.Broadcast(CooldownTag, -1.0f, -1.0f);
	}
}

异步事件:GE层数

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

  • 头文件
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

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;
}

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

void UAsyncTask_ListenGEStackChanged::EndTask()
{
	if (IsValid(ASC))
	{
		ASC->OnActiveGameplayEffectAddedDelegateToSelf.RemoveAll(this);
		ASC->OnAnyGameplayEffectRemovedDelegate().RemoveAll(this);
	}

	SetReadyToDestroy();
	MarkPendingKill();
}

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

同理在移除的时候也广播

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);
}