Published on

GameplayAbilitySystem入门与实战(五):GameplayAbility(二)

Authors
  • avatar
    Name
    东哥
    Twitter

前言

前文已经大概了解了GA的大致内容,本片结合项目来详细使用一下技能.

因为技能可以是通过按键直接触发, 我们在GA中多数是播放动画来表示技能执行的, 所以我们需要一个一个方便我们GA使用的播放动画的节点

GAS中有很多已经封装好的异步节点, 很多都是非常有用的,如下图(太多无法截全)

image-20201217142941527

其中有一个PlayMontageAndWait的节点, 可以用这个来播放动画蒙太奇, 然后通过动画通知来开启攻击能力, 攻击者(或者武器)触发攻击事件, 然后触发对应的GE来达到伤害的目的

那么我们重新捋一下, 我们在GA中播放动画, 然后外面的动画通知和检测我们不管, 我们还在GA这里监听某些Tag的事件, 响应以后我们在这里应用GE效果(ActionRPG的思路),这样是不是更简单直观一点

照着这个思路我们来做一下

获取目标

思考一下, 我们释放技能的时候需要知道目标是谁, 最好需要有HitResult数据方便我们知道打击位置播放特效等效果

那么各类技能或者普通攻击获取这个信息的方式都可能不一样,所以我们就自定义一个类专门来用于获取目标数据

UCLASS(Blueprintable, meta = (ShowWorldContextPin))
class SUPERROAD_API USRTargetType : public UObject
{
	GENERATED_BODY()

public: 
	USRTargetType() {}

	UFUNCTION(BlueprintNativeEvent)
		void GetTargets(ASRCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, `TArray<FHitResult>`& OutHitResults, `TArray<AActor*>`& OutActors) const;
};

这个类就专门提供给蓝图重写, 默认不需要实现什么内容

  • TargetingCharacter: 释放技能的角色

  • TargetingActor: 释放技能的对象, 可能是角色,也可能是武器或者投掷物等

  • EventData: 即FGameplayEventData,用于扩展参数

  • 蓝图重写

image-20201218102443161

如上图, 我们做一个最简单的box检测, 把找到的目标返回出去.

至于怎么用, 我们后面将

创建新的数据结构

前言说了, 我们播放动画然后需要收到事件信息, 理论上我们可以如下图这样做

image-20201218102759524

但是这样太笨拙, 而且WaitGameplayEvent返回的参数也不是很方便我们执行GE,所以我们把相关数据结构整合一下然后后面再创建一个新的整合版的异步事件

FSRGameplayEffectContainer

这个数据会作为TMap的值放到GA的配置中,保存了一个SRTargetType类,即前面讲的获取目标↑

USTRUCT(BlueprintType)
struct SUPERROAD_API FSRGameplayEffectContainer
{
	GENERATED_BODY()

public:
	FSRGameplayEffectContainer() {}

	/********** 目标类型*********/
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = GameplayEffectContainer)
		`TSubclassOf<USRTargetType>` TargetType;

	/** 应用到目标上的GE */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = GameplayEffectContainer)
		TArray<`TSubclassOf<UGameplayEffect>`> TargetGameplayEffectClasses;
};

然后我们到我们的GA基类SRGameplayAbilityBase中声明变量

	//标签对应的GE信息
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = GameplayEffects)
		TMap<FGameplayTag, FSRGameplayEffectContainer> EffectContainerMap;

FSRGameplayEffectContainerSpec

用来处理SRGameplayEffectContainer数据传递的结构体,参考GE和GA等都会有类似的Spec类

	/** 目标数据 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = GameplayEffectContainer)
		FGameplayAbilityTargetDataHandle TargetData;

	/** 应用到目标的GESpec */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = GameplayEffectContainer)
		`TArray<FGameplayEffectSpecHandle>` TargetGameplayEffectSpecs;

几个函数有必要看一下, 因为后面马上我们会用到

bool FSRGameplayEffectContainerSpec::HasValidEffects() const
{
	return TargetGameplayEffectSpecs.Num() > 0;
}

bool FSRGameplayEffectContainerSpec::HasValidTargets() const
{
	return TargetData.Num() > 0;
}

void FSRGameplayEffectContainerSpec::AddTargets(const `TArray<FHitResult>`& HitResults, const `TArray<AActor*>`& TargetActors)
{
	for (const FHitResult& HitResult : HitResults)
	{
        //hit类型的目标数据
		FGameplayAbilityTargetData_SingleTargetHit* NewData = new FGameplayAbilityTargetData_SingleTargetHit(HitResult);
		TargetData.Add(NewData);
	}

	if (TargetActors.Num() > 0)
	{
        //actor数组的目标数据
		FGameplayAbilityTargetData_ActorArray* NewData = new FGameplayAbilityTargetData_ActorArray();
		NewData->TargetActorArray.Append(TargetActors);
		TargetData.Add(NewData);
	}
}

异步事件

前言大概提了一下GAS中已经封装了很多继承自AbilityTask的异步事件, 我们找到UAbilityTask_PlayMontageAndWait类, 在此基础上扩展一些参数就可以了

创建类UAbilityTask_PlayMontage : public UAbilityTask

申明如下代理

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSRPlayMontageWaitEventDlg, FGameplayTag, EventTag, FGameplayEventData, EventData);

这个代理决定我们异步事件回调函数的参数

UPROPERTY(BlueprintAssignable)
		FSRPlayMontageWaitEventDlg OnCompleted;
	UPROPERTY(BlueprintAssignable)
		FSRPlayMontageWaitEventDlg OnBlendOut;
	//打断
	UPROPERTY(BlueprintAssignable)
		FSRPlayMontageWaitEventDlg OnInterrupted;

	/** 被执行 CancelAbility */
	UPROPERTY(BlueprintAssignable)
		FSRPlayMontageWaitEventDlg OnCancelled;

	/** 触发匹配Tag的Event*/
	UPROPERTY(BlueprintAssignable)
		FSRPlayMontageWaitEventDlg EventReceived;

上面参数用了我们自己的代理, 除了最后一个其余是模仿UAbilityTask_PlayMontageAndWait

然后一路模仿, 实现关于事件监听和广播

FDelegateHandle EventHandle;			
EventHandle = ASC->AddGameplayEventTagContainerDelegate(EventTags, FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(this, &UAbilityTask_PlayMontage::OnGameplayEvent));

void UAbilityTask_PlayMontage::OnGameplayEvent(FGameplayTag EventTag, const FGameplayEventData* Payload)
{
	if (ShouldBroadcastAbilityTaskDelegates())
	{
		FGameplayEventData TempData = *Payload;
		TempData.EventTag = EventTag;

		EventReceived.Broadcast(EventTag, TempData);
	}
}

完成!

image-20201218154751973

封装激活GE事件

我们测试一下, 我们通过动画通知到角色, 然后角色调用SendGameplayEventToActor后成功走到了我们测试的GA

image-20201218110426836

image-20201218110739309

但是蓝图中用这个返回参数来应用GE还是够呛

那么接下来就是封装可以利用这两个返回参数能执行相应GE效果的函数


  • ApplyEffectContainerSpec
`TArray<FActiveGameplayEffectHandle>` USRGameplayAbilityBase::ApplyEffectContainerSpec(const FSRGameplayEffectContainerSpec& ContainerSpec)
{
	`TArray<FActiveGameplayEffectHandle>` AllEffects;

	// 遍历GE并应用
	for (const FGameplayEffectSpecHandle& SpecHandle : ContainerSpec.TargetGameplayEffectSpecs)
	{
		AllEffects.Append(K2_ApplyGameplayEffectSpecToTarget(SpecHandle, ContainerSpec.TargetData));
	}
	return AllEffects;
}

前面我们已经申明了自己的数据结构,通过这个FSRGameplayEffectContainerSpecK2_ApplyGameplayEffectSpecToTarget执行GE, 那我们还需要一个构建这个Spec参数

  • MakeEffectContainerSpecFromContainer
FSRGameplayEffectContainerSpec USRGameplayAbilityBase::MakeEffectContainerSpecFromContainer(const FSRGameplayEffectContainer& Container, const FGameplayEventData& EventData, int32 OverrideGameplayLevel /*= -1*/)
{
	FSRGameplayEffectContainerSpec resultSpec;
	AActor* OwningActor = GetOwningActorFromActorInfo();
	ASRCharacterBase* OwningChar = `Cast<ASRCharacterBase>`(OwningActor);
	USRAbilitySystemComponent* OwningASC = `Cast<USRAbilitySystemComponent>`(GetAbilitySystemComponentFromActorInfo());

	if (OwningASC)
	{
		//TargetType有指定
		if (Container.TargetType.Get())
		{
			`TArray<FHitResult>` HitResults;
			`TArray<AActor*>` TargetActors;
			const USRTargetType* TargetTypeCDO = Container.TargetType.GetDefaultObject();
			AActor* AvatarActor = GetAvatarActorFromActorInfo();
			//获取目标数据
			TargetTypeCDO->GetTargets(OwningChar, AvatarActor, EventData, HitResults, TargetActors);
			//添加目标数据, 此目标会被应用GE
			resultSpec.AddTargets(HitResults, TargetActors);
		}
		//OverrideGameplayLevel = -1
		if (OverrideGameplayLevel == INDEX_NONE)
		{
			OverrideGameplayLevel = GetAbilityLevel();
		}

		for (const  `TSubclassOf<UGameplayEffect>`& c : Container.TargetGameplayEffectClasses)
		{
			//构建并添加GE到Spec内
			resultSpec.TargetGameplayEffectSpecs.Add(MakeOutgoingGameplayEffectSpec(c, OverrideGameplayLevel));
		}
	}
	return resultSpec;
}

这个方法目标是从SRTargetType类得到目标类和Hit数据, 通过FSRGameplayEffectContainerSpec::AddTargets()添加到TargetData数据中

但是这个函数是通过FSRGameplayEffectContainerEventData创建Spec, 还不是特别的方便

接下来我们补充一个通过Tag来构建Spec的辅助函数

  • MakeEffectContainerSpec
FSRGameplayEffectContainerSpec USRGameplayAbilityBase::MakeEffectContainerSpec(FGameplayTag ContainerTag, const FGameplayEventData& EventData, int32 OverrideGameplayLevel /*= -1*/)
{
	FSRGameplayEffectContainer* FoundContainer = EffectContainerMap.Find(ContainerTag);

	if (FoundContainer)
	{
		return MakeEffectContainerSpecFromContainer(*FoundContainer, EventData, OverrideGameplayLevel);
	}
	return FSRGameplayEffectContainerSpec();
}

关键就是从我们GA配置的Map变量去找到对应的值, 那这个函数就是我们比较方便使用的

那如果我们希望通过异步事件的返回参数直接应用GE, 我们每次要执行两个函数, 比较麻烦,接下来把两个函数合到一起

  • ApplyEffectContainer
`TArray<FActiveGameplayEffectHandle>` USRGameplayAbilityBase::ApplyEffectContainer(FGameplayTag ContainerTag, const FGameplayEventData& EventData, int32 OverrideGameplayLevel /*= -1*/)
{
	FSRGameplayEffectContainerSpec Spec = MakeEffectContainerSpec(ContainerTag, EventData, OverrideGameplayLevel);
	return ApplyEffectContainerSpec(Spec);
}

测试

我们的测试GE就扣一点血,如下图

image-20201218155846401

录制_2020_12_18_16_00_23_999

成功应用

补充若干重要的结构体数据

FGameplayEventData

传递数据的一个结构体,包含了诸多信息, 也有两个专门针对GATag栏的标记(SourceTag,TargetTag)

//事件tag
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayAbilityTriggerPayload)
	FGameplayTag EventTag;

	/** 发起者 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayAbilityTriggerPayload)
	const AActor* Instigator;

	/** 目标*/
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayAbilityTriggerPayload)
	const AActor* Target;

	/** 扩展类 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayAbilityTriggerPayload)
	const UObject* OptionalObject;

	/** 扩展类2 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayAbilityTriggerPayload)
	const UObject* OptionalObject2;

	/** GE上下文,请参考GE部分 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayAbilityTriggerPayload)
	FGameplayEffectContextHandle ContextHandle;

	/**发起者的Tag */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayAbilityTriggerPayload)
	FGameplayTagContainer InstigatorTags;

	/** 目标tag */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayAbilityTriggerPayload)
	FGameplayTagContainer TargetTags;

	/**这个事件的修改参数 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayAbilityTriggerPayload)
	float EventMagnitude;

	/** 目标数据 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayAbilityTriggerPayload)
	FGameplayAbilityTargetDataHandle TargetData;

目标数据/FGameplayAbilityTargetData

这是一个基类,仅提供了一些虚函数

目的是传递发起者和目标的基本信息

FGameplayAbilityTargetDataHandle

处理前者数据的类,一般在传递数据时候使用

保存了FGameplayAbilityTargetData的数组

FGameplayAbilityTargetData_LocationInfo

继承自FGameplayAbilityTargetData,

保存了发起者和目标的位置信息,用FGameplayAbilityTargetingLocationInfo保存

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
	FGameplayAbilityTargetingLocationInfo SourceLocation;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
	FGameplayAbilityTargetingLocationInfo TargetLocation;

FGameplayAbilityTargetData_ActorArray

继承自FGameplayAbilityTargetData,

保存了发起者和目标数组

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Targeting)
	FGameplayAbilityTargetingLocationInfo SourceLocation;
UPROPERTY(EditAnywhere, Category = Targeting)
	TArray<`TWeakObjectPtr<AActor>` > TargetActorArray;

#### FGameplayAbilityTargetData_SingleTargetHit

继承自FGameplayAbilityTargetData,

保存了Hit信息

UPROPERTY()
	FHitResult	HitResult;
UPROPERTY()
	bool bHitReplaced = false;

FGameplayAbilityTargetingLocationInfo

用不同的格式保存位置信息

  • 位置类型
namespace EGameplayAbilityTargetingLocationType
{
	enum Type
	{
		/** 提取实际位置信息,这是最后的备选方案 */
		LiteralTransform		UMETA(DisplayName = "Literal Transform"),

		/**从相关actor中得到变换信息 */
		ActorTransform			UMETA(DisplayName = "Actor Transform"),

		/**从骨骼模型中的插槽提取变换信息 */
		SocketTransform			UMETA(DisplayName = "Socket Transform"),		
	};
}
//位置类型
UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Targeting)
	TEnumAsByte<EGameplayAbilityTargetingLocationType::Type> LocationType;

	/** 如果技能超过了计算范围, 那么会使用这个信息 */
	UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Targeting)
	FTransform LiteralTransform;

	/** 基于actor的目标需要源actor,而基于socket的目标不需要源actor. */
	UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Targeting)
	AActor* SourceActor;

	/**基于插槽的目标需要骨架网格组件来检查指定。 */
	UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Targeting)
	UMeshComponent* SourceComponent;

	/** 使用数据的GA */
	UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Targeting)
	UGameplayAbility* SourceAbility;

	/** 如果SourceComponent有效,这是将使用的Socket转换的名称。如果没有提供Socket,将使用SourceComponent的转换。 */
	UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = true), Category = Targeting)
	FName SourceSocketName;