【USparkle專欄】如果你深懷絕技,愛“搞點研究”,樂於分享也博採眾長,我們期待你的加入,讓智慧的火花碰撞交織,讓知識的傳遞生生不息!
前言
最近在用UE做單機ARPG的戰鬥系統,研究了一下GAS。本文主要介紹GAS各個模組的用途,以及特定功能的多種實現方法。為了讓大部分人能快速上手,不會涉及太多C++和網路同步的內容。
推薦快速過一遍Unreal官方的GAS外掛介紹再來食用本文。
[中文直播]第31期|GAS外掛介紹(入門篇)
[UnrealOpenDay2020]深入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)