文章目录
GAS官方视频https://www.bilibili.com/video/BV1X5411V7jh
GAS文档https://docs.unrealengine.com/5.0/zh-CN/using-gameplay-abilities-in-unreal-engine/
堡垒之夜也是用的这个框架
其他的技能系统:
- Able Ability System
- Ascent Combat Framework (ACF)
GA流程
sample
在模块中要引用
PrivateDependencyModuleNames.AddRange(
"GameplayAbilities",
"GameplayTags",
"GameplayTasks",
}
character要继承IAbilitySystemInterface接口,以此被GAS识别
在character中声明一个UAbilitySystemComponent(ASC)组件
实现接口方法:
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
在构造函数中对ASC使用CreateDefaultSubObject去实例化
在character中去声明一个Ability数组去存储各种ability
UPROPERTY()
TArray<TSubclassof<UGameplayAbility>> MyAbilities;
像是打击受击掉血的流程,有两个能力GA-Attack和BeAttack,有一个GE behitt描述了伤害
属性(AS)中如血量等值不是float,而是FGameplayAttributeData结构体,有base值和cur值,方便GE(buff)作用后的数值回滚。
MakeOutgoingGameplayEffectSpec指可以定一泛类型GE的一个模板,然后在GE生成时读表或者数据库动态改变。更好的是通过IDetailCustomization去定制GA的UI。
GAS中文文档https://blog.csdn.net/pirate310/article/details/106311256
GAS英文文档https://github.com/tranek/GASDocumentation
教学博客https://www.cnblogs.com/JackSamuel/p/7155500.html
大钊-深入GAS框架
https://www.bilibili.com/video/BV1zD4y1X77M?spm_id_from=333.999.0.0
l
ASC对于单机游戏可以放在Pawn上,对于联机游戏可以放在playstate上以广播和持续的保存
GASSimple
链接:https://pan.baidu.com/s/10U3QOB0oz7vMT1B-IXbMMw 提取码:uenb
Tag
先看看核心的tag是怎么标定的:
比较简单,定义了一层,分别是攻击,格挡,受击,主动
ASC
GASSample中ASC是挂载在character上的,在构造函数中初始化
注意这个Character继承了IAbilitySystemInterface
接口,其中只有一个GetAbilitySystemComponent方法用于返回此Actor上的ASC,通过这种方式让此Actor被GAS系统所识别和使用。
class AGASSampleCharacter : public ACharacter,
public IAbilitySystemInterface //修改:继承接口
{
GENERATED_BODY()
public:
AGASSampleCharacter();
protected:
//修改:添加BeginPlay
virtual void BeginPlay() override;
public:
// 修改:申明ASC
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = GameplayAbilities, meta = (AllowPrivateAccess = "true"))
class UAbilitySystemComponent* AbilitySystem;
// 修改:实现接口方法
UAbilitySystemComponent* GetAbilitySystemComponent()const override;
// 修改:声明Ability数组
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Abilities)
TArray<TSubclassOf<UGameplayAbility>> MyAbilities;
//此actor的属性集
UPROPERTY()
USampleAttributeSet* AttributeSet;
}
AGASSampleCharacter::AGASSampleCharacter()
{
// 修改:实例化ASC
AbilitySystem = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystem"));
// 在OwnerActor的构造方法中创建的AttributeSet将会自动注册到ASC
AttributeSet = CreateDefaultSubobject<USampleAttributeSet>(TEXT("AttributeSet"));
}
void AGASSampleCharacter::BeginPlay()
{
Super::BeginPlay();
if (nullptr != AbilitySystem)
{
// 修改:给ASC赋予技能
if (HasAuthority() && MyAbilities.Num() > 0)
{
for (auto i = 0; i < MyAbilities.Num(); i++)
{
if (MyAbilities[i] == nullptr)
{
continue;
}
AbilitySystem->GiveAbility(FGameplayAbilitySpec(MyAbilities[i].GetDefaultObject(), 1, 0));
}
}
// 修改:初始化ASC
AbilitySystem->InitAbilityActorInfo(this, this);
}
}
注意GA是通过GiveAbility
赋给ASC的,一个GA相当于一个模板,通过使用一个GameplayAbility
以及相关参数如等级,生成一个能力实例FGameplayAbilitySpec
。
FGameplayAbilitySpecHandle UAbilitySystemComponent::GiveAbility(const FGameplayAbilitySpec& AbilitySpec);
这个基础的GASSampleCharacter有三个GA,GA是游戏逻辑的主要书写处
GA
看看GA_Attack_Attr以此为例,梳理下流程。
GA_Attack_Attr的ability tag为positive,代表这是个主动效果,并且BlockAbilitiesWithTag也是positive tag,代表这个tag存在时会阻塞其他的positive tag。
这里通过GetActorInfo拿到此GA拥有者的相关信息,包括OwnerActor(攻击方)、AvatarActor(受击方)、SKMesh、ASC等。
在这个攻击动画蒙太奇中有一个NotifyName为Hit的event,播放攻击蒙太奇触发HitEvent,判断是否找到AvatarActor。
找到则在受击者的ASC上触发一个受击的效果GE_hit以及收到伤害扣血的效果GE_Damage。
在调用蒙太奇后直接调用了CommitAbility
,以提交技能进行计算如技能消耗等,这里不commit在场景中攻击就不会扣蓝。
CommitAbility的流程可以看下面,可以知道CommitAbility是最后可以判定GA失败的时机,不然之后GA就一定成功。
在蒙太奇播完后主动触发结束GE的event,GE结束时停止蒙太奇。
这里播放蒙太奇使用的是UE原生的节点,有说法是UE原生节点在网络同步时会有问题,一定要使用GAS的蒙太奇节点
GAS蒙太奇组合了tag和event,更加系统方便,同时还有自动结束蒙太奇的功能,比起原生节点更加强大,还是使用这个吧。。。
GE
GA_Attack_Attr定义了攻击的消耗属性即GE_Cost(demo中的体现即为5点蓝耗)
应用策略是Instant立即生效(Infinite是持续生效,HasDuration是一定时间后生效),GE设置简单的对SampleAttributeSet.Physical(这个属性是需要代码自定义的)属性即蓝量应用计算 Add -5,
应用概率ChanceToAppleToTarget为1,必定生效。
AS
来看看这个SampleAttribute如何在代码中定义,一开始定义了一个宏
// 定义一个增加各种Getter和Setter方法的宏
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
进入内部看看,作用注释。官方注释说得很清楚了,在自己的AS中使用ATTRIBUTE_ACCESSORS宏。
/**
* This defines a set of helper functions for accessing and initializing attributes, to avoid having to manually write these functions.
* It would creates the following functions, for attribute Health
*
* static FGameplayAttribute UMyHealthSet::GetHealthAttribute();
* FORCEINLINE float UMyHealthSet::GetHealth() const;
* FORCEINLINE void UMyHealthSet::SetHealth(float NewVal);
* FORCEINLINE void UMyHealthSet::InitHealth(float NewVal);
*
* To use this in your game you can define something like this, and then add game-specific functions as necessary:
*
* #define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
* GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
* GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
*
* ATTRIBUTE_ACCESSORS(UMyHealthSet, Health)
*/
#define GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
static FGameplayAttribute Get##PropertyName##Attribute() \
{
\
static FProperty* Prop = FindFieldChecked<FProperty>(ClassName::StaticClass(), GET_MEMBER_NAME_CHECKED(ClassName, PropertyName)); \
return Prop; \
}
#define GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
FORCEINLINE float Get##PropertyName() const \
{
\
return PropertyName.GetCurrentValue(); \
} //当前值
#define GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
FORCEINLINE void Set##PropertyName(float NewVal) \
{
\
UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); \
if (ensure(AbilityComp)) \
{
\
AbilityComp->SetNumericAttributeBase(Get##PropertyName##Attribute(), NewVal); \
}; \
}//设置属性新值
#define GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) \
FORCEINLINE void Init##PropertyName(float NewVal) \
{
\
PropertyName.SetBaseValue(NewVal); \
PropertyName.SetCurrentValue(NewVal); \
}//进行初始化
看一下SimpleAttributeSet的简单框架,以属性physic(蓝耗)为例。
一个属性集AttributeSet集成了多个属性FGameplayAttributeData,从而使得这一套属性可以在不同的ASC上使用。
属性的基本类型是FGameplayAttributeData而非float,FGameplayAttributeData中定义了currentvalue和basevalue,用于处理buff和数值回滚的情况。
UCLASS()
class GASSAMPLE_API USampleAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
//网络同步
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
public:
UPROPERTY(BlueprintReadOnly, Category = "Physical", ReplicatedUsing = OnRep_MaxPhysical)
FGameplayAttributeData Physical; //定义蓝耗属性值
ATTRIBUTE_ACCESSORS(USampleAttributeSet, Physical) //定义属性的getset和init接口
UFUNCTION()
void OnRep_MaxPhysical(const FGameplayAttributeData& OldValue);//属性同步时客户端调用
public:
// 属性修改前回调
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue);
// GE执行后属性回调
virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
};
在PreAttributeChange函数中对新设置的值进行范围限定,在PostGameplayEffectExecute进行值改变后的回调处理,这里直接调用AvatarActor进行相关处理不是好的写法,只是简单demo示例
void USampleAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
...
DOREPLIFETIME(USampleAttributeSet, Physical); //对蓝耗进行网络同步
}
//进行范围限定
void USampleAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
if (Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
}
if (Attribute == GetPhysicalAttribute())
{
NewValue = FMath::Clamp(NewValue, 0.f, GetMaxPhysical());
}
}
void USampleAttributeSet