【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!
前言
最近在用UE做单机ARPG的战斗系统,研究了一下GAS。本文主要介绍GAS各个模块的用途,以及特定功能的多种实现方法。为了让大部分人能快速上手,不会涉及太多C++和网络同步的内容。
推荐快速过一遍Unreal官方的GAS插件介绍再来食用本文。
文末附参考文献。
一、GameplayTags与GAS
1.1 GameplayTags
FGameplayTags是一种层级标签,如Parent.Child.GrandChild。
通过GameplayTagManager进行注册。替代了原来的Bool,或Enum的结构,可以在玩法设计中更高效地标记对象的行为或状态。
GameplayTags是一个内置的插件,不属于GAS。
但是GAS会大量使用Tag,在编辑-项目设置里可以找到。
GA、GE、GameplayEvent、GameplayCue都会大量使用Tag。如果你还不懂这些名词的含义,可以先往后看。
同时角色本身的枚举、布尔等状态(非属性)变量也可以用Tag储存,非常好用,我自己的用法是在让AI在行为树的Decorator节点中,通过玩家拥有的GameplayTag来判断玩家是否处于无敌帧、喝药。
但因此Tag的层级关系也需要合理设计,到了后期修改成本比较大。
个人的建议是设计Ability、Effect、GameplayCue、Event、Character、Cooldown等基础标签,在对应的模块只检测对应的Tag(比如监听动画通知发送的Event,就可以是Event.AnimNotify.Fire),避免重复或者模糊定义。
举一个反面例子,比如将Attack的GA标签和GE标签、角色处于攻击状态的标签都设置为Character.Attack,不利于追述Tag的来源。
其他的Tag可以根据需求添加,如Item等。
同时要注意,Tag的父子关系也要考虑的,若合理设计,可以用于批量筛选同一类Tag。
在Editor内批量修改Tag可能不是那么方便,这里有一个比较好的管理方法:
《Ue4Config方法总结与另外一种GameplayTag管理方法》
1.2 Gameplay Ability System
GAS主要包含以下内容:
- ASC(Ability System Component)主要组件,由C++编写,代码里有很多方法是蓝图未实现的。
- GA(Gameplay Abilities)角色的技能,包括攻击、疾跑、施法、翻滚、使用道具等,但不包括基础移动和UI。
- AS(Attribute Set)角色身上可以用float表示的属性,如生命值、体力值、魔力值等。
- GE(Gameplay Effects)用于修改属性,如增加50移动速度10s;还能配合GA实现更多玩法。
- GC(Gameplay Cues)播放特效、音效等。
如果看过《深入GAS架构设计》,可以发现应该还有Task和Event两个额外功能。
严格意义上这两个功能和Tag一样是UE原生内容,在GA部分比较常用,因此本文选择将其放在GA部分讲解。
二、Ability System Component
2.1 ASC组件介绍
Ability System Component(ASC)是整个GAS的基础组件。
ASC本质上是一个UActorComponent,用于处理整个框架下的交互逻辑,包括使用技能 (GameplayAbility)、包含属性(AttributeSet)、处理各种效果(GameplayEffect)。
所有需要应用GAS的对象(Actor),都必须拥有GAS组件。
拥有ASC的Actor被称为ASC的OwnerActor,ASC实际作用的Actor叫做AvatarActor。ASC可以被赋予某个角色ASC,也可以被赋予PlayerState(可以保存死亡角色的一些数据)
简单来说,ASC是一种角色组件,负责和GA、GE、AS打交道。
一般只放在Character or PlayerState上,在武器上加ASC组件也不是不行,但是并没有很好的实践供参考,官方文档提到过这一点。
OwnerActor和AvartarActor是比较常见的概念,如果ASC在Character类身上,那么二者是相同的。
如果Character需要销毁再重新生成,如MOBA游戏角色死亡后泉水复活,那么ASC可以放在PlayerState上避免随着角色一同销毁。此时的OwnerActor是PlayerState,AvatarActor则是Character。
学习动画系统后的一些补充想法:如果希望角色部分为纯蓝图实现,以便直接指定父类为一些模板角色蓝图,如ALSv4,这种情况下也许放在PlayerState里会更好?
2.2 添加ASC组件
一开始的设置需要一些C++,这里IDE建议使用Rider for Unreal,会自动补全一些头文件声明,防止遗漏。
C++基础知识补充:
UE的C++类分为两部分,一个是.h头文件,一个是.cpp文件。
属性和方法的声明写在.h文件里,方法的实现写在.cpp里,包括构造函数。
在进行接下来的操作之前,你需要自己创建一个继承自ACharacter的C++类,如下图的ARPGCharacterBase。(图源右下角)
之后想用蓝图,就从这个自定义的C++Character类派生就可以了。
图源@开发游戏的老王
1、角色.h中声明ASC。
#include "AbilitySystemComponent.h"
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities")
class UAbilitySystemComponent* AbilitySystemComponent;
不要忘了在项目Build.cs文件的PrivateDependencyModuleNames里加上“GameplayAbilities”,“GameplayTags”,“GameplayTasks”三个模块。
2、在.cpp中构造函数部分实例化ASC。
//实例化ASC
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystem"));
3、角色类继承IAbilitySystemInterface接口,并实现GetASC函数。
#include "AbilitySystemInterface.h"
class ARPG_UNREAL_API ACharacterBase : public ACharacter, public IAbilitySystemInterface
public:
UAbilitySystemComponent* GetAbilitySystemComponent()const override;
.cpp
UAbilitySystemComponent* ACharacterBase::GetAbilitySystemComponent()const{return AbilitySystemComponent;}
上面的示例代码使用的是原生的ASC组件。如果想自己继承一个ASC子组件封装一些功能,可以参考官方的Action RPG实例项目里的写法。
2.3 ASC组件的功能
GAS的大部分功能都在ASC组件的源码中,并且只有一部分暴露给了蓝图,有些功能如添加GA(见3.2)还需要通过代码实现。由于本文只是一篇快速入门手册,不再过多赘述。
想要详细了解GAS系统,可以先从ASC组件的源码入手,有时可以避免重复造轮子。
具体的功能会在下文使用到。
三、Gameplay Ability
3.1 GA介绍
Gameplay Ability(GA)标识了游戏中一个对象(Actor)可以做的行为或技能。能力(Ability)可以是普通攻击或者吟唱技能,可以是角色被击飞倒地,还可以是使用某种道具,交互某个物件,甚至跳跃、飞行等角色行为也可以是Ability。
Ability可以被赋予对象或从对象的ASC中移除,对象同时可以激活多个GameplayAbility。*基本的移动输入、UI交互行为则不能或不建议通过GA来实现
一个GA蓝图大概就长这样
角色需要拥有GA后,才能使用GA。
GA的使用分为实例化和释放两个过程,前者主要是生成一个FGameplayAbilitySpec对象,并为一部分非公有(非静态)属性赋值,如当前GA的等级。后者操作的实际对象则为Spec。
可以把Spec理解为GA的实例,GE等其他类也有相似的概念。
通常来说,使用GA时不用去考虑两个过程的区别,除非你需要在实例化Spec后,手动修改一些在GA类上定义好的属性再去手动释放。在GE篇会详细介绍,用于实现技能的冷却、消耗。
3.2介绍GA的不同获得方式,3.3介绍GA蓝图的制作,3.4介绍GA的使用。
3.2 添加GA
如果不使用C++修改,只能通过GE去添加GA,非常不方便。
这里介绍两种修改方法。
3.2.1 在角色类中创建一个数组,游戏启动时自动添加数组里的GA
注意:使用这一种方法不易控制每个GA的初始等级。
1、在角色头文件声明数组:
public:
// 将在游戏启动时被赋予角色的Abilities数组
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Abilities")
TArray<TSubclassOf<class UGameplayAbility>> PreloadedAbilities;
2、在角色的BeginPlay()里遍历数组,使用AbilitySystemComponent->GiveAbility()添加Ability:
Super::BeginPlay();
if (AbilitySystemComponent != nullptr)
{
//初始化技能
if (PreloadedAbilities.Num() > 0)
{
for (auto i = 0; i < PreloadedAbilities.Num(); i++)
{
if (PreloadedAbilities[i] != nullptr)
{
// FGameplayAbilitySpec是GA的实例,其构造函数的第二个参数代表GA的等级,这里暂令其全部为1
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(PreloadedAbilities[i].GetDefaultObject(), 1));
}
}
}
//初始化ASC
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
3、在角色蓝图的Details面板找到数组,填好GA。
如果后面做完一个GA发现没反应,可能就是忘记Give刚做好的GA给角色了,或者场景中的角色对象拥有的技能没有和类默认值同步。
3.2.2 在角色蓝图中使用Give Ability函数手动添加Ability
上面提到的AbilitySystemComponent->GiveAbility()方法在蓝图中无法使用。
为了在蓝图中动态添加Ability,我们需要在蓝图中实现自己的GiveAbility()。
按理说在自己的ASC子类中实现最好,这里在角色蓝图中实现。
1、CharacterBase.h
public:
//添加Ability
UFUNCTION(BlueprintCallable, Category = "Ability System")
void GiveAbility(TSubclassOf<UGameplayAbility> Ability, int32 Level = 1);
2、CharacterBase.cpp
void ACharacterBase::GiveAbility(TSubclassOf<UGameplayAbility> Ability, int32 Level)
{
if (AbilitySystemComponent)
{
if (HasAuthority() && Ability)
{
AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(Ability, Level));
}
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
}
使用例
这也是一个将ASC中未暴露给蓝图的函数进行封装的例子,如果想在蓝图中使用其他的ASC函数可以进行参考。
3.2.3 使用GE添加GA
新建一个GE,在Granted Abilities条目里添加的GA都会在GE被Apply到角色身上时赋予(Grant)。
参数的具体含义详见5.2.10。
然后在蓝图里调用Apply GameplayEffect to Self节点即可(这里的Level是GE的等级,不是GA)。
3.3 制作GA
在内容浏览器右键,创建Gameplay→Gameplay技能蓝图:
继承自GameplayAbility:
最下面两个是内置的GA,上面的是自制的GA
GA蓝图的结构
一般GA要做的事有:
- 设置GA的Tag、CD、Cost等属性。
- 获取必要信息,主要通过Get Actor Info。如果是通过Event调用的GA(使用Activate Ability From Event节点作为输入),还可以通过Gameplay Event Data获取。
- 编写逻辑,如播放动画、应用GE、应用冲量等。
- 一定不要忘了EndAbility。
3.4 调用GA
我把GA的调用分成了主动调用(释放技能)和被动调用(挨打)两类,下面依次介绍不同的调用方法。
3.4.1 主动调用
在蓝图中主要有by Class和by Tag两种调用方法。
byClass一次只能Activate一个GA,byTag可以Activate任意多个GA,配合Tag容器使用。
如果使用EnhancedInputAction插件来管理输入,要注意在某些设置下Trigger会每帧都进行输出(本人测试环境为4.27,UE5似乎有一些改动。古代山谷项目就使用了新版输入插件和GAS系统,可以看一看实现方法)。
只要能获取ASC,就可以在任何地方调用GA,比如行为树Task蓝图,甚至在GA蓝图中调用其他GA。
3.4.2 被动调用
Trigger可以理解为一个Tag,当ASC组件收到一个Trigger时,就会自动调用所有拥有该Trigger的GA。
Trigger的Tag在GA的Details面板中设置。
Trigger的触发方式有三种,分别是:
- Gameplay Event:当Owner收到一个带有Tag的Gameplay Event(不是Gameplay Effect的GE!)时调用一次GA,此时Owner不会拥有对应的Tag。
- Owner Tag Added:当Owner获取对应Tag的时候调用一次GA。
- Owner Tag Present:当Owner拥有Tag时调用GA,失去Tag时移除。
一般使用第一种方法,并配合SendGameplayEventToActor节点使用,如下图所示。(这张图是很久以前截的,Tag建议以Event开头。)
受击效果的例子,发送一个Tag为Hit的Event给碰撞检测到的Actor
使用Gameplay Event调用的好处是,可以传入数据(Payload),是除了Get Actor Info外的另一种信息传递方法。
此时应该删除ActiveAbility节点,转而使用ActivateAbilityFromEvent事件。(不要通过在左上角重载函数的方式,右键空白处搜索才是对的。)
3.5 设置GA触发条件
3.5.1 GA的标签
可以限制各种技能的相互关系,比如受击时候不能翻滚。
这时候Tag的父子层级关系设计就尤为重要,可以把受击时不能释放的技能都放在同一个父层级下。
Tag建议以Ability开头。
Ability Tags:该GA的标签。
Cancel Abilities with Tag:激活该GA时,打断其他拥有所选标签的GA。
Block Abilities with Tag:激活该GA时,阻止激活拥有所选标签的GA(已经激活的不会被中断)。
Activation Owned Tags:激活该GA时,赋予ASC所选GA。
Activation Required Tags:激活GA时,ASC需要的标签。
Activation Blocked Tags:激活GA时,ASC不能有的标签。
Source Required Tags:激活GA时,Source需要的标签。
Source Blocked Tags:激活GA时,Source不能有的标签。
Target Required Tags:激活GA时,Target需要的标签。
Target Blocked Tags:激活GA时,Target不能有的标签。
上图就是一个防止重复触发GA的简单设置。
3.5.2 冷却与消耗
想要添加冷却与消耗,就需要写好对应的GE,建议先看完GE篇。
在GA的Details面板的Cost和Cooldown条目中选择对应的GE即可。
一个Cooldown GE仅需满足以下要求:
- 为Has Duration类型,Duration Magnitude计算方式为Set By Caller或Custom Calculation Class。
- Granted Tags为技能的冷却Tag,如Cooldown.skill1。
在Cooldown GE持续期间,玩家的ASC组件就会携带对应技能的Cooldown Tag,本质是通过Tag来限制的。
*冷却Tag建议以Cooldown开头统一管理。
一个Cost GE仅需满足以下要求:
- 为Instant类型。
- 有一个或多个Modifier去修改对应的属性,计算方式为Custom Calculation Class。
但这样一来每个GA都要写一遍Cost和CD的GE,非常麻烦。
官方文档4.5.14和4.5.15小节有介绍优化方法:
https://github.com/tranek/GASDocumentation#concepts-ge-cost
这里参考官方文档简单地实现一下,原理为在实例化生成GE Spec时,修改其Cost和Cooldown属性后再将其应用。
首先创建一个GA基类,添加CD时长、Cost数值(包括生命值和法力值两种类型的Cost)、以及Cooldown Tag等属性,并重载GetCooldownTags、ApplyCooldown、GetCostGameplayEffect三个方法。
GameplayAbilityBase.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "GameplayAbilityBase.generated.h"
/**
*
*/
UCLASS()
class ARPG_UNREAL_API UGameplayAbilityBase : public UGameplayAbility
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldowns")
FScalableFloat CooldownDuration;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldowns")
FGameplayTagContainer CooldownTags;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Costs")
FScalableFloat HealthCost;
// 根据需要可以设置多种类型Cost
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Costs")
FScalableFloat ManaCost;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Costs")
FScalableFloat StaminaCost;
// Temp container that we will return the pointer to in GetCooldownTags().
// This will be a union of our CooldownTags and the Cooldown GE's cooldown tags.
UPROPERTY(Transient)
FGameplayTagContainer TempCooldownTags;
// Return the union of our Cooldown Tags and any existing Cooldown GE's tags.
virtual const FGameplayTagContainer* GetCooldownTags() const override;
// Inject our Cooldown Tags and to add the SetByCaller to the cooldown GameplayEffectSpec.
virtual void ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const override;
virtual UGameplayEffect* GetCostGameplayEffect() const override;
};
GameplayAbilityBase.cpp
#include "ARPG_Unreal/Public/GameplayAbilitySystem/GameplayAbilityBase.h"
const FGameplayTagContainer* UGameplayAbilityBase::GetCooldownTags() const
{
FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
MutableTags->Reset();
// MutableTags writes to the TempCooldownTags on the CDO so clear it in case the ability cooldown tags change (moved to a different slot)
const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
if (ParentTags)
{
MutableTags->AppendTags(*ParentTags);
}
MutableTags->AppendTags(CooldownTags);
return MutableTags;
}
void UGameplayAbilityBase::ApplyCooldown(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo) const
{
UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
if (CooldownGE)
{
FGameplayEffectSpecHandle SpecHandle =
MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CooldownTags);
SpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Cooldown")),
CooldownDuration.GetValueAtLevel(GetAbilityLevel()));
ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
}
}
UGameplayEffect* UGameplayAbilityBase::GetCostGameplayEffect() const
{
return Super::GetCostGameplayEffect();
}
然后创建继承UGameplayModMagnitudeCalculation,创建对应属性的Cost MMC,这里仅展示法力值消耗,Stamina消耗同理。
ManaMMC.h
#pragma once
#include "CoreMinimal.h"
#include "GameplayModMagnitudeCalculation.h"
#include "ManaMMC.generated.h"
/**
*
*/
UCLASS()
class ARPG_UNREAL_API UManaMMC : public UGameplayModMagnitudeCalculation
{
GENERATED_BODY()
virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
};
ManaMMC.cpp
#include "GameplayAbilitySystem/ManaMMC.h"
#include "GameplayAbilitySystem/GameplayAbilityBase.h"
float UManaMMC::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
const UGameplayAbilityBase* Ability = Cast<UGameplayAbilityBase>(Spec.GetContext().GetAbilityInstance_NotReplicated());
if (!Ability)
{
return 0.0f;
}
return Ability->ManaCost.GetValueAtLevel(Ability->GetAbilityLevel());
}
最后写一个通用的Cost和CD GE,所有的GA都使用这两个GE创建Spec。
Cost GE
Cooldown GE
注意上面的Data Tag并不等同于Cooldown Tag,只是用于告诉GE的修改器(Modifier)需要修改(Modify)的Data是什么。Cooldown Tag才是CD期间拥有的Tag,以Cooldown开头。
之后创建一个GA蓝图基类,之后所有的GA都继承自这个基类,配置好CD、Tag和Cost,然后调用Commit Ability节点就好了。如果不需要Cost或CD,最好取消选择Cooldown GE Class和Cost GE Class,以避免当魔力值归零时无法释放0消耗技能的问题。
基类设置
具体GA配置
3.6 Ability Task
GA是在一帧内完成的,如果想要实现类似Wait的异步逻辑需要使用Task。
图中所示就是Ability Task,是基于原生的Gameplay Task实现的。
可以看见,GAS内置了许多Task,图中用的是一个播放蒙太奇的Task(注意与UE原生的播放蒙太奇节点不同,在GAS系统中最好使用PlayMontageAndWait)。
自带的Task的功能讲解可以参考这篇文章。
《GAS AbilityTask节点功能整理》
如果想要实现自己的Task,如监听玩家输入等,需要使用C++。具体实现可以参考这篇文章,不再赘述。
《虚幻四Gameplay Ability System入门(12)-Ability Task》
四、AttributeSet
4.1 Attribute与AS
AttributeSet负责定义和持有属性,并且管理属性的变化,包括网络同步。需要在Actor中被添加为成员变量,并注册到ASC(C++)。
一个ASC可以拥有一个或多个(不同的)AttributeSet,因此可以角色共享一个很大的 AttributeSet,也可以每个角色按需添加AttributeSet。
可以在属性变化前(PreAttributeChange)后(PostGameplayEffectExecute)处理相关逻辑,可以通过委托的方式绑定属性变化。
正如字面意思,AS是Attribute的集合。
Attribute就是HP、MP、Speed、ATK等可以用float表示的属性。
因为Attribute是包含了两个float变量的结构体,分别是Base Value和Current Value。
Base Value表示基础值,Current Value表示临时值。
如临时增加100生命值10s,改变的就是Current Value,10s后自动变回Base Value。
做GE时要注意修改的是哪种Value(详见5.2.1)。
AS只能使用C++创建。
4.2 AS添加
创建AttributeSetBase类,这里需要使用AbilitySystemComponent.h的宏ATTRIBUTE_ACCESSORS()。
对每一个FGameplayAttributeData都应用一遍宏。
这里创建Health和MaxHealth作为示范。
AttributeSetBase.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "AttributeSetBase.generated.h"
// Uses macros from AttributeSet.h
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
/**
*
*/
UCLASS()
class ARPG_UNREAL_API UAttributeSetBase : public UAttributeSet
{
GENERATED_BODY()
public:
// Attributes
UPROPERTY(VisibleAnywhere, BlueprintReadWrite);
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UAttributeSetBase, Health);
UPROPERTY(VisibleAnywhere, BlueprintReadWrite);
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UAttributeSetBase, MaxHealth);
};
最后在ASC组件里指定好就可以使用了。
Default Starting Table是用于属性初始化用的
(详见下一节)
4.3 AS初始化
AS可以通过GE和DataTable两种不同方式初始化,Epic推荐使用GE。
4.3.1 通过GE初始化
建议先看完GE部分。
创建一个GE,命名为GE_InitAttributes,找到Gameplay Effect条目。
一个Modifiers对应一个Attributes。
添加新的Modifier,选择要修改的属性,Modifier Op(修改方式)选择Override。
Modifier Magnitude(修改值)选择Scalable Float,填入想设置的默认值。
然后在蓝图中Apply该GE即可。
4.3.2 通过DataTable初始化
创建一个DataTable,行结构选择AttributeMetaData,其格式如下。
可以使用Excel做好后保存为csv文件再快速导入。
注意这里的最大最小值没有任何作用,为未完成功能,实现方法见4.5。
因此最好单独创建一个MaxHealth属性。
属性的名称需要带上完整类名。
然后找到Character的ASC组件,在Attribute Test条目填上表格即可。
4.4 AS获取
搜索Get Attribute即可,Current Value和Base Value都可以获得。
可以通过ASC组件调用,也可以使用GAS的蓝图函数库里的函数。
4.5 监听Attribute修改事件
建议先看完GE部分,了解GE的机制后再回来看此部分。
4.5.1 PreAttributesChange和PostGameplayEffectExecute
AttributeSet提供了两个方法用于监听Value的改变:
- PreAttributeChange:用于Attribute的Current Value被改变前调用,对应Infinite和Has Duration的GE。
- PostGameplayEffectExecute:用于Base Value改变后调用,对应InstantGE。
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
这两个事件适用于Clamp属性,确保其不超出临界值。
void UAttributeSetBase::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
}
}
// 这个方法也行,但是需要"GameplayEffectExtension.h"
void UAttributeSetBase::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
if(Data.EvaluatedData.Attribute == GetHealthAttribute())
{
SetHealth(FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth()));
}
}
4.5.2 GetGameplayAttributeValueChangeDelegate
如果想要监听Attribute的变化以更新UI,则不适合用上面的方法,应该在角色类中创建一个回调,以及蓝图事件:
CharacterBase.h
// Attribute Change Callbacks
void OnHealthChanged(const FOnAttributeChangeData& Data);
// Attribute Change Event in Blueprint
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChangeEvent, float, NewHealth);
UPROPERTY(BlueprintAssignable, Category="Ability")
FOnHealthChangeEvent HealthChangeEvent;
CharacterBase.cpp
void ACharacterBase::OnHealthChanged(const FOnAttributeChangeData& Data)
{
HealthChangeEvent.Broadcast(Data.NewValue);
}
然后在BeginPlay()里将其注册到ASC:
void ACharacterBase::BeginPlay()
{
Super::BeginPlay();
if (AbilitySystemComponent != nullptr)
{
//初始化技能...
//初始化ASC...
//注册Attribute变化事件
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UAttributeSetBase::GetHealthAttribute()).AddUObject(this, &ACharacterBase::OnHealthChanged);
}
}
之后就可以从蓝图调用生命值变化事件了。
五、Gameplay Effect
5.1 GE介绍
Gameplay Effect(GE)是Ability对自己或他人产生影响的途径。GE通常可以被理解为我们游戏中的Buff。比如增益/减益效果(修改属性)。
但是GAS中的GE也更加广义,释放技能时候的伤害结算,施加特殊效果的控制、霸体效果 (修改GameplayTag)都是通过GE来实现的。
GE相当于一个可配置的数据表,不可以添加逻辑。开发者创建一个UGameplayEffect的派 生蓝图,就可以根据需求制作想要的效果。
GE就是一张数据表,不负责逻辑处理,定义Attribute修改的值。
GE是修改Attribute的唯一渠道!
其工作流程可以简单分为以下几步:
- 创建一个实例Spec。
- (可选)修改Spec的一些值。
- 如果允许,应用(Apply)GE,但是Attribute仍未被修改。
- 如果允许,使Modifier生效,修改Attribute。
- 满足条件后,移除该Spec。
其功能非常多且强大,提供了非常多的可配置项。来一张图感受一下:
GE的配置项可以满足绝大多数游戏的需求,尤其是MOBA游戏和RPG游戏。
比较重要的功能有嵌套调用GE、赋予GA、调用GC。
此外也能根据等级计算数据、实现多层GE叠加、设置GE应用的条件、几率等。
5.2介绍上面的大部分配置项,5.3介绍GE的核心配置——Modifier,5.4介绍GE的使用。
5.2 GE配置项讲解
5.2.1 Gameplay Effect
最核心的配置项。
Duration Policy:GE的持续类型,有三种。
- Instand:立即改变Base Value(扣血)。
- Infinite:永久改变Current Value(按下疾跑修改速度)只能通过GA或ASC取消。
- Has Duration:临时修改Current Value(临时Buff)。
Modifiers:选择你要修改的Attribute,支持数值等级曲线和Tag,会单独讲。
Executions:同样也是修改属性,支持更复杂的运算。
Conditional Gameplay Effects:当GE成功应用时,可以应用其他GE,嵌套调用GE的方法之一。
如果是Has Duration的GE,那么我们需要设定Duration的时长,称为Duration Magnitude。
而下文的Modifier里面也有一个Magnitude的概念,二者的设置方法是一样的,详见5.3.2。
5.2.2 Period
设置GE的触发周期,仅有Infinite和Has Duration的GE才显示前两项设置。
Period:
如果是Infinite模式,加上Period后等价于周期执行的Instant;
如果是Has Duration模式,就是普通的周期重复。(也有说法是周期执行的Instance,待证实。)
Execute Periodic Effect on Application:t=0的时候是否触发。(如LOL中点燃技能就是使用后立即造成伤害,之后每秒应用一次)。
Periodic Inhibition Policy:GE中断并恢复后的处理方式。
- Never Reset:从被打断时的位置开始计算周期,相当于暂停再播放。
- Reset Period:从0开始计算周期。
- Execute and Reset Period:打断时立即执行一次,下次从0开始计算周期。
5.2.3 Application
设置GE的应用概率和条件。
概率支持曲线图表。
条件可以简单地用Tag去限制,也可以用Application Requirement进行更复杂的逻辑判断。
需要添加一个自定义的Custom Application Requirement(CAR)蓝图类,重载里面的唯一方法,如下图所示。
官方文档推荐在以下情况使用CAR蓝图类:
- Target需要有一定数量的属性时;
- Target需要GE堆叠到一定数量时;
- 除此之外CARs还能够做更多事情,比如检查Target是否应用了一个GameplayEffect 的实例,在应用一个新实例时如果同类型的实例已存在则只改变其持续时间(CanApplyGameplayEffect()要返回false)。
5.2.4 Stacking
用于叠加多个GE的效果,仅能用于Infinite和Has Duration的GE。
Stack Limit Count:最大层数。
Stacking Type:叠加栈在目标身上or施法者身上。
举个例子,假设层数为3,如果是by Target模式,那么3个敌人对我释放的Debuff只能叠三层。
如果是by Source模式,那么3个敌人可以对我叠加9层Debuff。
每层Effect如果是Modifiers来计算,则为直接叠加的效果,比如用Modifiers来增加3攻击力,则第一层为增加3攻击力,第二层为增加6攻击力,第三层为增加9攻击力,而如果需要根据层数不同而改变增加的值,则需要使用Executions。
Stack Duration Refresh Policy:Apply新GE时是否刷新持续时间,注意溢出的Apply也会刷新,想关闭可以在下面的Overflow条目关闭。
Stack Period Reset Policy:同上,是否刷新周期。
Stack Expiration Policy:当一层GE的Duration到期后的处理方式。
- Clear Entire Stack:清空全部层数,如LOL征服者。
- Remove Single Stack and Refresh Duration:清空一层,如LOL致命节奏。
- Refresh Duration:不清空,相当于无限长的Duration,但可以通过调用FActiveGameplayEffectsContainer::OnStackCountChange(FActiveGameplayEffect& ActiveEffect, int32 OldStackCount, int32 NewStackCount)方法来自己处理细节,如一次掉两层。
5.2.5 Overflow
可以设置Stack溢出会Apply的GE。通过GE应用GE的方法之一,需要配合Stacking来使用。
Deny Overflow Application:如果为True,则溢出的Apply不会刷新Duration。
Clear Stack On Overflow:字面意思,需要勾选上一个选项后才能选中。
5.2.6 Expiration
当GE的Duration被打断或结束时的行为。通过GE应用GE的方法之一,仅能用于Has Duration的GE。
Premature Expiration Effect Classes:打断时Apply的GE。
Routine Expiration Effect Classes:正常结束时Apply的GE。
5.2.7 Immunity
Immunity和Tag类似,也可以用来限制GE。
通过Tag匹配来实现,匹配的目标是Target的ASC组件以及拥有的GA。
如果拥有Require Tags的所有Tag,并且没有Ignore Tags的所有Tag,则认为匹配成功,该GE不会被Apply。
和Tags相比,Immunity提供了一个回调UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate。
下面的Granted Application Immunity Query是更高级的匹配,但是更消耗性能。
比较特殊的是最后三个选项,分别是根据GE修改的Attribute匹配、根据GE来源匹配以及根据GE的类匹配。
5.2.8 Tags
和GA的Tag条目类似,设置各种限制条件。
GameplayEffectAssetTag:GE的Tag。Combined Tag为计算结果,不可编辑,计算方式是继承的Tag+Added-Removed。
GrantedTags:GE会赋予目标ASC的Tag,仅适用于Infinite和Has Duration的GE。
Application Tag Requirements:GE满足Tag条件才能应用(Apply)。
Ongoing Tag Requirements:GE满足Tag条件才能修改值(Modifier or Execution)。通过这项设置GE可以仅Apply而不修改值,仅适用于Infinite和Has Duration的GE。
Removal Tag Requirements:GE满足Tag条件就会被移除。
Remove Gameplay Effects with Tags:Apply后移除指定Tag的GE。
Remove Gameplay Effect Query:上面一条的高级版,可以匹配GE的类(Effect Definition),匹配来源(Effect Source)以及匹配GE修改的属性(Modifying Attribute),移除成功匹配的GE。
5.2.9 Display
与特效相关的设置,调用Gameplay Cue的方式之一,详见6.3.1。
5.2.10 Granted Abilities
使用GE添加GA的方式。仅支持Infinite和Has Duration的GE。
Level:GA的等级。
Input ID:如果使用旧版输入,每一个操作映射都对应着一个枚举值,输入对应的枚举值就可以将这个新GA绑定到输入上。
Removal Policy:设置当GE被移除时,GA是否要移除。
- Cancel Ability Immediately:移除,并触发事件EndAbility。
- Remove Ability on End:移除,但是不触发EndAbility。
- Do Nothing:GA不会被移除。
5.3 Modifier与Execution
5.3.1 Modifier是什么
Modifier在Gameplay Effect目录下,作用是修改Attribute,一个Modifier对应一个Attribute。
Attribute:要修改的Attribute,AttributeSetBase是自己写的C++ AS类。
Modifier Op:运算符(Operator),有加、乘、除和覆盖四种。
Modifier Magnitude:运算值,与Attribute进行Modifier Op选择的运算。
Tags:是否能修改该Attribute的限制条件,这里指ASC组件上的Tag。
我们制作GE,主要任务就是确定Attribute、Op与Magnitude,而Magnitude是最灵活的一部分,可以通过四种方式得到,下面即将介绍。
5.3.2 Magnitude的计算
Magnitude的计算方式有四种,对应四种Magnitude Calculation Type:
- Scalable Float:不计算,直接给定一个浮点数作为Magnitude的值,也可以从等级曲线中获得。要注意的是如果使用了曲线图表,图表里获得的值会和输入的数相乘。
- Attribute Based:读取玩家或目标属性作为一个值,可以进行简单线性计算。
主要分为三个部分,上面的部分是运算系数,公式为:
要修改的Attribute和用来计算Magnitude的Attribute是不一样的,为了区分这里称后者为Attr。Coe,Pre,Post都可以通过等级图表获得。中间的部分为Attr的来源,图例为目标的生命值。可以实现如偷取敌方最大生命值20%的效果。AttributeCurve的存在意义不是很清楚。
根据《GameplayEffect(一)功能》的说明,正确的公式应该如下,有待测试:
关于Snapshot,官方文档是这么说明的:
快照(Snapshot)意味着取GameplayEffectSpec被创建时属性的值,否则取GameplayEffectSpec被应用时属性的值。
正常情况下,我们为角色应用GE需要调用ApplyGEToOwner节点(详见5.4.1),此时系统会自动帮我们创建一个Spec实例并将其Apply。
但我们也可以手动地调用MakeOutgoingGESpec节点来实例化一个GE,修改其值后,再使用ApplyGESpecToOwner节点应用该Spec(该方法在3.5.2有使用)。
如果想要获取修改前的值,就可以勾选Snapshot。
下面的Attribute Calculation Type有三种,代表Attr使用的值:
- Attribute Magnitude:使用Current Value。
- Attribute Base Value:字面意思。
- Attribute Bonus Magnitude:使用Current Value - Base Value。
Tag也是计算限制条件,不过是对Attr的限制,而不是上文的Attribute。
- Custom Calculation Class:自定义的更复杂的运算规则,与AttributeBase相比好处是可以获取任意数量的Attr。
点击Calculation Class旁边的加号,将创建一个GameplayModMagnitudeCalculation蓝图类。
里面唯一的重载函数就是写计算逻辑的地方,返回的float就是Magnitude值。
此外还有一个继承的变量RelevantAttributeToCapture,可以在类默认值设置要Capture的Attribute及其来源。
但这个蓝图应该只是半成品,Spec和GE Attribute Capture Definition结构体都没法拆分,想要在蓝图使用还需要去C++部分自己实现一些函数给蓝图。
如果使用C++的方式编写MMC,可以参考这篇文章:
《虚幻四Gameplay Ability System入门(7)-Gameplay Effect详解(2)自定义Calculation Class》
对照着上图理解会更直观一些。同时在3.5.2处设置Cost时也有一个使用MMC的例子。
- Set By Caller:通过蓝图获得Magnitude。
一般情况下,我们Apply一个GE后,系统会自动帮我们生成一个GE的Spec并添加到目标的ASC上。5.4会说明GE的一般使用方法。
这里的思路不太一样,我们先是创建了一个GE的实例Spec,用Caller修改指定Modifier的Magnitude之后,再将Spec Apply到目标上。
而Data Tag则用于区分多个Modifier,告诉蓝图修改哪个Modifier的Magnitude,建议用Data开头。
比较简单,也非常好用。配置完之后可以从GA或是ASC按照下图所示方法使用该GE。
图例为创建一个Cooldown GE的实例后,再将Cooldown值赋给对应Data的Magnitude。Cooldown的实现并不是这样,这里只是一个演示,可以自行换成其他属性。
5.3.3 Execution介绍
更高级的Modifier,一个Execution就能设置多个Attribute。
和上面计算Modifier的Magnitude用的CalculationClass类似,区别在于上文用到的MagnitudeCalculation是获取多个Attr以计算Magnitude,再通过Magnitude修改Attribute。
而这个ExecutionCalculation是直接获取多个Attribute进行修改。
具体实现可以参考5.3.2的第3小节。
此外,Conditional GE可以设置Execution成功执行时候Apply的GE,这也是非主动应用GE的方法之一。
图的最下面也有一个Conditional GE,注意二者是不一样的,在5.2.1有提到。鼠标悬停也能查看二者的差别。
5.4 GE的应用
GE的应用我们称为Apply,可以从GA或者ASC去Apply一个GE。
GE可以通过蓝图手动应用,也可以通过GE的配置项,使GE在特定条件下嵌套应用其他GE。
5.4.1 GE的主动应用
应用GE的时候,我们可以设置GE的等级。
Stacks表示应用多少层的GE,仅在GA里Apply GE时才能设置此项。
注意重复调用该节点也算多层GE叠加,Stack详见5.2.4。
GA中Apply GE的例子
ASC中Apply GE的例子
Apply的对象有Owner也有Target,Owner比较省事。
如果想让敌人扣血,可以在GA里先Send一个Gameplay Event,通过Event调用Target的播放受击动画的GA,再在GA里Apply一个扣血GE To Self。
5.4.2 GE的嵌套应用
涉及3个配置项,对应不同的条件:
- Gameplay Effect:当前GE成功应用后,应用配置好的GE,见5.2.1。
- Overflow:GE层数溢出时,应用配置好的GE,可以做满层爆炸的效果,见5.2.5。
- Expiration:GE中断或结束时,应用配置好的GE,见5.2.6。
六、Gameplay Cue
6.1 GC介绍
GameplayCues(GC)执行非游戏性相关的事情,比如音效,粒子特效,震屏等。GameplayCues通常会被复制和预测(除非设置Executed, Added或Removed是本地的)。
主要有Static和Actor两类GC。
Static适用于单次播放的特效。由于其是静态的,不会产生实例,因此在其蓝图里创建的变量都是只读的。对应Instant和Periodic的GE。
Actor适用于持久的,不定时长的特效。其继承自一个场景Actor,每次使用会产生一个对应实例。对应Infinity和Has Duration的GE。
6.2 GC的制作
打开窗口-GameplayCue编辑器,可以看到如下页面:
每一个GC(处理器/Handler)需要一个对应的Tag,点击新增会显示GC蓝图创建页面。
根据需求选择即可。
不用GameplayCue编辑器,直接创建GC蓝图也是可以的,但是记得在类默认值中设置Tag。
6.2.1 Static类型GC设置
对于Static的GC,仅需重载OnExecute函数即可。
通过获取传入参数Target的根组件,就可以附加粒子系统发射器了。
Parameter用于传入一些参数,如伤害飘字的数值等,具体的设置会在6.3.1说明。
6.2.2 Actor类型GC设置
Actor类型GC继承自场景Actor,因此有Tick、BeginPlay、Overlap等其他函数。
因此其类默认值也有很多设置,最主要的是Gameplay Cue和CleanUp两个目录。
这里我们重载OnActive和OnRemove两个函数即可。
和Static类型的GC不同的是,如果我们勾选了Gameplay Cue的Auto Attach GC To Owner,我们可以用GC自身的根组件作为发射器要附加的组件(在不需要绑定到指定骨骼的情况下)。
此外,由于Actor类型的GC是非静态的,可以产生实例,因此是可以创建变量并写入的。
6.3 GC的调用
一般通过GE配置,也可以在GA里调用Execute/Add触发。
6.3.1 从GE配置GC
选择GC对应的Tag即可,可以同时选择多个Tag,触发多个GC。
Require Modifier Success to Trigger Cues:需要GE成功修改Attribute后才调用GC,而不仅仅是Apply该GE。
Suppress Stacking Cues:多个GE存在Stack中时是否实例化多个GC(如果使用了Stack,对应的一定是可以实例化的Actor类GC)。
Min、Max Level和Magnitude Attribute则与传入参数有关。
Raw Magnitude即为Magnitude Attribute的值,而Normalized Magnitude的计算方式如下:
$Normalized = (Raw - Min) / (Max - Min)$
如上图所示,当Min=0且Max=100时,Normalized = Raw / 100,即百分比。
6.3.2 从GA调用GC
共有五个相关函数,Add&Remove与Execute分别对应Actor类型和Static类型的GC,具体用法见图。
七、Debug方法
可以参考《GameplayAbility中的Debug方法》。
八、总结
整个GAS系统的工作流程如图所示。
ASC管理GA、GE、Attribute。
GE可以用来给予ASC一个GA,也可以修改Attribute。(甚至还能Apply其他的GE,图中没有提到。)
GA可以发送Event给其他ASC,调用对应的GA;也可以对目标Apply一个GE,修改其属性。
GE和GA都可以用来触发GC。
如果看到这里所有内容都明白了,那么可以看[UnrealOpenDay2020]深入GAS架构设计 | EpicGames 大钊,系统讲解了GAS的整体框架,读源码会更容易。
参考文献
- https://github.com/tranek/GASDocumentation
- 虚幻引擎游戏技能系统文档_玉田白菜的博客-CSDN博客
- Gameplay技能系统 | 虚幻引擎文档(unrealengine.com)
- [中文直播]第31期|GAS插件介绍(入门篇) | 伍德 大钊_哔哩哔哩_bilibili
- [UnrealOpenDay2020]深入GAS架构设计 | EpicGames 大钊_哔哩哔哩_bilibili
- 虚幻四教程 - 知乎(zhihu.com)
- GAS AbilityTask节点功能整理
- GameplayAbility中的Debug方法
- 虚幻插件GAS分析3-0 GameplayEffect标签面面观-标签栏
- [玩转UE4/UE5动画系统>技能系统(GAS)篇] 二 技能 Gameplay Ability(GA)
- 虚幻引擎游戏技能系统 - 知乎 (zhihu.com)
- Unreal Engine GAS系统_虎牙维护世界和平-CSDN博客
- 8.10学习记录,UE4官方GASdemo,ARPG_StrangerMQ的博客-CSDN博客
- UE4 GAS/ActionRPG学习导图——AttributeSet(Gameplay Ability System)_jkchen's Haven-CSDN博客
这是侑虎科技第1584篇文章,感谢作者LunarMaxim供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/LunarMaxim
再次感谢LunarMaxim的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。