GameplayAbilitySystem入门与实战(四):GameplayAbility(一)

前言

在第一篇初始化的时候简单讲解了技能的添加和使用, 但是没有对技能GameplayAbility(GA)做详细介绍,本片开始对GA中的主要功能注意剖析

注册/移除技能

之前我们通过配置的方式自动注册起始技能, 那么我们肯定需要动态的增加或者删除技能, 为了方便使用, 封装蓝图库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UFUNCTION(BlueprintCallable, Server, Reliable, WithValidation, Category = "SR|ASC")
void RPC_Ser_AddAbility(TSubclassOf<USRGameplayAbilityBase> AbilityClass, int32 level = 1);
UFUNCTION(BlueprintCallable, Server, Reliable, WithValidation, Category = "SR|ASC")
void RPC_Ser_RemoveAbilityByClass(TSubclassOf<USRGameplayAbilityBase> AbilityClass);
UFUNCTION(BlueprintCallable, Server, Reliable, WithValidation, Category = "SR|ASC")
void RPC_Ser_RemoveAbilityByName(const FString& name);

//以下3个非RPC事件
UFUNCTION(BlueprintCallable, Category = "SR|ASC")
bool AddNewAbility(TSubclassOf<USRGameplayAbilityBase> AbilityClass, int32 level = 1);
UFUNCTION(BlueprintCallable, Category = "SR|ASC")
bool RemoveAbilityByClass(TSubclassOf<USRGameplayAbilityBase> AbilityClass);
UFUNCTION(BlueprintCallable, Category = "SR|ASC")
bool RemoveAbilityByName(const FString& name);
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
bool USRAbilitySystemComponent::AddNewAbility(TSubclassOf<USRGameplayAbilityBase> AbilityClass, int32 level)
{
if (AbilitySpecMap.Num()>0)
{
for (TPair<FString, FGameplayAbilitySpec*> p : AbilitySpecMap)
{
if (p.Value->Ability->GetClass() == AbilityClass)
{
UE_LOG(SRLog, Warning, TEXT("USRAbilitySystemComponent::AddNewAbility Failed!! Has Repeat Ability"));
return false;
}
}
}
FGameplayAbilitySpec spec = FGameplayAbilitySpec(AbilityClass, level, static_cast<int32>(AbilityClass.GetDefaultObject()->InputID), GetOwnerActor());
GiveAbility(spec);
AbilitySpecMap.Add(AbilityClass.GetDefaultObject()->AbilityName, this->FindAbilitySpecFromClass(AbilityClass));
return true;
}

bool USRAbilitySystemComponent::RemoveAbilityByClass(TSubclassOf<USRGameplayAbilityBase> AbilityClass)
{
FGameplayAbilitySpec* spec = FindAbilitySpecFromClass(AbilityClass);
if (spec)
{
this->ClearAbility(spec->Handle);
for (TPair<FString, FGameplayAbilitySpec*> p : AbilitySpecMap)
{
if (p.Value->Ability->GetClass() == AbilityClass)
{
AbilitySpecMap.Remove(p.Key);
}
}
return true;
}
UE_LOG(SRLog, Warning, TEXT("USRAbilitySystemComponent::RemoveAbilityByClass Failed!! Do not has this ability"));
return false;
}

这个Name版本是我们在GA类中自定义的, 通过TMap<FString, FGameplayAbilitySpec*> AbilitySpecMap保存在我们ASC

image-20201226141212117

触发技能

如果是已经用之前按键绑定的技能, 那么按键就会直接触发技能

还有其他几个方法来手动触发技能

1
2
3
4
5
6
7
8
9
10
11
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilitiesByTag(const FGameplayTagContainer& GameplayTagContainer, bool bAllowRemoteActivation = true);

UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilityByClass(TSubclassOf<UGameplayAbility> InAbilityToActivate, bool bAllowRemoteActivation = true);

bool TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation = true);

bool TriggerAbilityFromGameplayEvent(FGameplayAbilitySpecHandle AbilityToTrigger, FGameplayAbilityActorInfo* ActorInfo, FGameplayTag Tag, const FGameplayEventData* Payload, UAbilitySystemComponent& Component);

FGameplayAbilitySpecHandle GiveAbilityAndActivateOnce(const FGameplayAbilitySpec& AbilitySpec);

到蓝图中就是如下

image-20201218084759424

激活技能的不同方法决定GA中的函数调用, 一般都是执行到ActiveAbility,

如果通过事件的方式触发技能, 即上述中的TriggerAbilityFromGameplayEvent或者蓝图中的SendGameplayEventToActor,那么可以提供一个Payload参数作为扩展参数,这个非常有用

那么在GA中可以重写函数ActivateAbilityFromEvent,前提是不要重写默认的ActivateAbility函数;

通过事件触发的方式还会与后续的tag有关系, 这个后续再讲

image-20201203145317457

技能逻辑没有固定规则, 完全可以自己脑洞, 不过千万不要忘记在技能完成以后调用EndAbility来结束技能,否则技能一直结束不了而类似一个被动技能一直存在

被动技能

被动技能如果是默认就存在(触发),可以在GA中的OnAvatarSet事件中最判断处理,如果有必要就直接调用TryActivateAbility(我们之前已经申明了一个布尔变量bAutoActive), 然后不要调用EndAbility就可以了

1
2
3
4
5
6
7
8
void UVGGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec)
{
Super::OnAvatarSet(ActorInfo, Spec);
if (bAutoActivate)//bool变量交给蓝图配置
{
ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, false);
}
}

关闭/中断技能

目前在ASC中有如下几个方法关闭技能,都未暴露给蓝图;

1
2
3
4
5
6
7
void CancelAbility(UGameplayAbility* Ability);	

void CancelAbilityHandle(const FGameplayAbilitySpecHandle& AbilityHandle);

void CancelAbilities(const FGameplayTagContainer* WithTags=nullptr, const FGameplayTagContainer* WithoutTags=nullptr, UGameplayAbility* Ignore=nullptr);

void CancelAllAbilities(UGameplayAbility* Ignore=nullptr);

在蓝图中只能在GA中自己调用CancelAbility关闭技能

不过我们可以自己封装蓝图函数库,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*稍微注意一点,这里的FGameplayTagContainer参数我们使用引用而不是AbilityComponent::CancelAbilities函数中的指针,顺便来个AutoCreateRef都是方便蓝图使用*/
void UFlib_GAS::CancelAbilityWithTag(UAbilitySystemComponent* AbilityComponent, const FGameplayTagContainer& WithTags, const FGameplayTagContainer& WithoutTags, UGameplayAbility* Ignore)
{
if (!AbilityComponent)
{
return;
}
AbilityComponent->CancelAbilities(&WithTags, &WithoutTags, Ignore);
}

void UFlib_GAS::CancelAllAbilities(UAbilitySystemComponent* AbilityComponent, UGameplayAbility* Ignore /*= nullptr*/)
{
if (!AbilityComponent)
{
return;
}
AbilityComponent->CancelAllAbilities(Ignore);
}
//************************************

image-20201203164413337

GASDocumentation 项目文档说 CancelAllAbilities有时候无法正常生效,但是我这边实测可以正常关闭技能

获取激活的技能

可以通过方法 void GetActivatableGameplayAbilitySpecsByAllMatchingTags(const FGameplayTagContainer& GameplayTagContainer, TArray < struct FGameplayAbilitySpec* >& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements = true) const;根据tag获取正在运行的技能

照样可以封装一个蓝图函数库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//.h
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "VGAS | FunctionLib", meta = (AutoCreateRefTerm = "GameplayTagContainer, WithTags"))
static void GetActivatableGameplayAbilitySpecsByAllMatchingTags(UAbilitySystemComponent* AbilityComponent, const FGameplayTagContainer& GameplayTagContainer, TArray < struct FGameplayAbilitySpec >& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements);
//.cpp
void UFlib_VGAS::GetActivatableGameplayAbilitySpecsByAllMatchingTags(UAbilitySystemComponent* AbilityComponent, const FGameplayTagContainer& GameplayTagContainer, TArray < struct FGameplayAbilitySpec >& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements)
{

if (!AbilityComponent)
{
return;
}
TArray <FGameplayAbilitySpec* > returnAbilities;

AbilityComponent->GetActivatableGameplayAbilitySpecsByAllMatchingTags(GameplayTagContainer, returnAbilities, bOnlyAbilitiesThatSatisfyTagRequirements);
for (FGameplayAbilitySpec* i : returnAbilities)
{
MatchingGameplayAbilities.Add(*i);
}

}



这里还是因为蓝图的原因,传参不能按照原版本传参,需要一个临时变量returnAbilities小小的加工一下

image-20201203173016089

实例化模式

技能的实例化模式分为3种,见下表

Instancing Policy Description Example of when to use
Instanced Per Actor 每个ASC只会生成一个实例,每次触发GA时服用实例对象 这个是最常用的方式
Instanced Per Execution 每次GA激活都会产生一个实例 这个方式不太常用,但是可以每个技能都独立作用
Non-Instanced GA自己默认Obj来维护自身,不会生成实例 这是三种方法中性能最好的,但是在使用的时候是最受限制的。非实例化GameplayAbilities不能存储状态,这意味着没有动态变量,也没有绑定到AbilityTask委托。在MOBA或RTS中,最适合使用它们的是那些经常使用的简单技能,比如仆从基本攻击或者角色的Jump

网络方案

image-20201204152923024

Net Execution Policy Description
Local Only 只运行在本地客户端,这个比较适合在只表现本地效果的技能,单人游戏中需要使用Server Only
Local Predicted 先本地运行, 服务端会校准本地客户端错误的内容
Server Only 只在服务端运行, 本地技能适合运行在服务端,单人游戏也适合用此
Server Initiated 服务端先运行然后再运行在客户端,不常用

标签

GA的标签非常重要而且使用,包含如下标签

GameplayTag Container Description
Ability Tags 当前技能拥有的标签, 也是通过tag来激活技能的凭证
Cancel Abilities with Tag 当前技能会关闭的拥有此类tag的正在运行技能
Block Abilities with Tag 运行技能的会阻挡拥有此类tag的技能(并不会返回失败)
Activation Owned Tags 此技能会激活的tag
Activation Required Tags 激活技能所需要包含的tag
Activation Blocked Tags 当拥有此类tag的技能正在运行时,该技能会被阻挡(并不会返回失败)
Source Required Tags 此标签包括如下共4个标签内,都是通过ByEvent的方式作用才有效; 此标签在输入的时候必须包含才能运行
Source Blocked Tags 同理会被阻挡的标签
Target Required Tags 同理目标必须包含的tag
Target Blocked Tags 同理目标如果包含会被阻挡的tag

image-20201204170040609

Gameplay Ability Spec

Gameplay Ability SpecGA正确激活后会创建, 内部提供了诸多GA中的数据如 class,level,input bindings

如果GA在服务端创建, 会同步到客户端

Gameplay Ability Spec创建实例规则请参考实例化模式 ↑

GA的传递数据

GA传递外部数据有如下几种方式

Method Description
Activate GameplayAbility by Event 通过此方法激活技能会有一个Payload参数, 可以用来传递诸多参数;如两个UObject参数更加方便扩展自定义数据
Use WaitGameplayEvent AbilityTask 用这个方法可以监听其他tag技能的状态,以此类得到payload数据
Use TargetData 用结构体 TargetData 传递数据是一个办法,具体以后补充
Store Data on the OwnerActor or AvatarActor 把数据存到GAOwnerActor或者角色中,但是你需要确保网络同步

image-20201204171513695

技能消耗/冷却

技能的消耗和冷却通过GameplayEffect实现, 详情可以查看GameplayEffect篇

技能升级

Level Up Method Description
Ungrant and Regrant at the New Level 移除GA,重新注册一个等级不一样的GA,此方式会终止GA
Increase the GameplayAbilitySpec's Level spec中的level进行升级处理,这种方式不会终止GA

蓝图中无法得到GA中的Level变量, 所以无论是对GALevel进行处理还是GE都需要在cpp中

1
2
3
4
5
6
7
8
9
void USRAbilitySystemComponent::UpgradeAbilityByName(const FString& name, int32 upLevel)
{
if (AbilitySpecMap.Contains(name))
{
FGameplayAbilitySpec* spec = *AbilitySpecMap.Find(name);
spec->Level += upLevel;
this->MarkAbilitySpecDirty(*spec);
}
}

因为添加的时候已经存放了TMap<FString, FGameplayAbilitySpec*> AbilitySpecMap, 直接对其进行操作即可, 按照GAS注释说明需要调用MarkAbilitySpecDirty()

网络规则

NetSecurityPolicy Description
ClientOrServer 无要求,客户端和服务器都可以自由触发
ServerOnlyExecution 客户端请求执行将被服务器忽略。客户端仍然可以请求服务器取消或终止此功能。
ServerOnlyTermination 客户端可以请求执行, 但是不能请求终止或者取消
ServerOnly 只有服务端有权执行,客户端无法执行任何操作