UE5的GAS学习

记录一下UE5的GAS的学习内容

Posted by John Young on September 11, 2024

前言

最近在做自己项目的时候,遇到技能系统设计的一些技术选型问题。 考虑到自己这方面经验很少,因此我打算先学习一下业界的成熟解决方案。 Unity在这方面没有特别好的轮子,简单研究之后发现UE的Game Ability System在设计上和自己的原始想法比较接近,因此我决定深度学习一下UE5的GAS,查漏补缺地改进自己的设计,最终应用在自己的Unity项目中。

Note:这些只是我自己的主观总结,用于打开设计思路的,很多和引擎、网络同步相关的部分我都略过了。 我的主要参考资料是:GASDocumentationOfficial Ducument

1. 总体介绍

  • 适用场景:适用于Moba游戏或者RPG游戏,支持联网,开箱即用
  • 成功案例:Paragon, Fortnite
  • 框架模块:
    1. GameplayAbilities(基于等级的角色能力系统,支持资源消耗和冷却时间)
    2. Attributes(操纵Actor身上的属性数值)
    3. GameplayEffects(在Actor身上添加状态效果)
    4. GameplayTags(Actor身上的标签)
    5. GameplayCues(生成特效或音效)
    6. 上述所有内容的实例复制
  • 多人游戏支持下列客户端预测:
    1. 技能激活
    2. 播放timeline
    3. 属性改变
    4. 添加GameTags
    5. 生成GameplayCues
    6. 角色的移动

2. GAS中的概念

2.1 Ability System Component

ASC是一种Actor组件,它是GAS的核心。挂载了ASC的Actor才可以使用GameplayAbilities、拥有Attribute或者接收GameplayEffectsASC也管理了上述对象的生命周期、复制等等。

挂载ASCOwnerActor不一定和它的表现类AvatarActor是同一个Actor。对于MOBA游戏,OwnerActor一般是PlayerState类,而AvatarActor一般是Character类。OwnerActorAvatarActor如果不是同一个Actor,则需要实现GetAbilitySystemComponent()接口来实现相互通信。

ASC保存了Actor获得的所有Gameplay Abilities,遍历这些能力的时候,需要加锁ABILITYLIST_SCOPE_LOCK(),以确保不会删除列表中的能力(自己实现技能系统的话,可能技能列表的删除需要延迟处理?)。

ASC管理了GameplayEffectsGameplayTagsGameplayCues的网络同步(AttributesAttributesSet同步)。有三种同步模式Full, MixedMinimal。在单机、多人,以及是玩家操控还是AI操控Actor,使用的同步模式不同。

2.1 GameplayTags

GameplayTags是一些带有层级结构的名称的集合,以FName储存。例如:State.Debuff.Stun表示一个被晕眩的debuff。通过布尔逻辑就能知道物体是否带有某些tag。出于效率原因,tag的集合一般用GameplayTagContainers 而不是TArray<FGameplayTag>,因为FName的复制有冗余,前者有做优化。UE5需要通过ini文件预先定义所有的tags,可以在UE5编辑器里配置与编辑tags。

GameplayTags如果是由GameplayEffect添加的话,会是自动复制的。ASC可以添加和删除另一种LooseGameplayTags,它不会被复制,因此这种情况下,需要手动管理LooseGameplayTags。例如角色的死亡,需要客户端本地立即处理,可以手动添加State.DeadLooseGameplayTags;角色重生之后再手动删除这个LooseGameplayTags

ASC添加或者移除GameplayTag时,会有回调事件(可以同时添加多个同名tag到一个ASC,返回的变化后的TagCount):

AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged);
virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewTagCount);

2.2 Attributes

  1. Attributes的定义:Attributes储存float类型的数据,用来表示游戏里与某个Actor相关的各种数值(等级、生命值、法力值etc)。一般来说Attributes只能由GameplayEffects来修改,这样ASC才能预测其改变(网络同步相关)。Attributes存在于AttributeSet之中,由其定义、复制(只有标记了可复制的才会复制)。

  2. AttributesBaseValueCurrentValue两个值构成。BaseValue储存的是基础值,CurrentValue储存的是基础值经过GameplayEffects的临时修改之后的值。例如角色基础移速600,BaseValueCurrentValue在没有buff的情况下都是600;这时候如果有个加速buff,那BaseValue还是600,CurrentValue就是650了。BaseValue的永久改变来源于Instant GameplayEffects,而Duration GameplayEffectsInfinite GameplayEffects则只会改变CurrentValue

  3. Meta Attributes:一类需要与其他属性交互的临时Attributes。例如伤害值一般是Meta AttributesGameplayEffect不会直接改变我们的生命值,而是改变这个临时的伤害值属性;伤害值属性也能被buff和debuff影响,也能被AttributeSet中的其他属性影响(例如护甲值)。Meta Attributes一般不会被标记为需要复制。Meta Attributes的引入可以实现逻辑分离,Gameplay Effects不需要知道目标如何处理伤害,不同目标可能有不同的AttributeSet,例如有的没护甲属性,有的有护甲属性,它们可以有不同的处理伤害的方式。

  4. 属性变化也有回调事件:
    AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged);
    virtual void HealthChanged(const FOnAttributeChangeData& Data);
    

    FOnAttributeChangeData包含OldValueNewValue

  5. Derived Attributes(派生属性):派生属性一般是由一个或多个Attributes,通过一个Infinite GameplayEffect(拥有多个Attribute Based或者MMC Modifiers)派生出来的。当Attributes的值改变时,相应的Derived Attributes的值会自动更新。

2.3 Attribute Set

ASC通过AttributeSet定义、管理Attributes。一个ASC可以有一个或多个AttributeSetAttributeSet的开销是微不足道的,所以开发者可以比较自由地组织Attributes,既可以使用一个全局通用的AttributeSet,所有Actor都使用其中的某些属性;也可以不同类型Actor使用不同的AttributeSet

AttributeSet可以通过一个instant GameplayEffect来初始化属性(推荐做法)。

PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)AttributeCurrentValue改变之前的回调,这里适合对最终值做一些clamp。

PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)只会在BaseValueInstant GameEffect改变之后被触发。例如,伤害属性在设计上是一个Meta Attribute,可以监听它的这个事件,在这里去做扣除生命值、护甲的逻辑,以及受击动画、伤害飘字等等逻辑。

OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator)会在新的Aggregator创建时触发。Aggregator会基于所有的Modifier和属性的BaseValue去计算属性的CurrentValue。例如,减速buff会创建负面的Modifier,在所有减速Modifier中,只想减速程度最大的Modifier生效,这个时候就需要在Aggregator创建这个回调里,设置Aggregator的参数。

2.4 Gameplay Effects

GameplayEffects(GE)是Ability改变AttributesGameTags的一种载体。GE既可以造成立即的属性值改变(例如受伤、治疗),也可以添加长期的状态(例如眩晕、加速buff)。一个GE类只包含数据,不包含逻辑,一个游戏里一般有非常多的GE派生类。

GE通过ModifiersExecutions改变属性;GE有三种持续类型:Instant, DurationInfinite。另外,GE也可以添加或执行GameplayCuesInstant GE会调用GameplayCuesExecute事件,用于BaseValue的立即修改,不能添加GameTag;而Duration GEInfinite GE会调用GameplayCuesAdd/Remove事件,用于CurrentValue的临时修改,也可以添加GameTag,二者区别在于是否会expire。

Duration或者Infinite类型的GE还可以添加Periodic Effects,周期性地应用ModifiersExecutionsPeriodic Effects被视为Instant GE,可以改变BaseValue、执行GameplayCues,它对于制作DOT(Damage Over Time)类型的伤害效果很有用。

GE临时的开和关可以由GameplayTags控制,而不需要删掉这个GE的实例。关闭GE时可以添加一个关闭的GameplayTag,移除Modifiers;打开GE可以重新激活Modifiers,移除相应GameplayTag

GE是一般是不会被实例化的。当一个ASC想要添加一个GE时,会从GEClassDefaultObject创造一个GameplayEffectSpec(可以看作GE的实例)并添加到ASCActiveGameplayEffects中。

添加或移除GE有很多的接口,但最终都是走到TargetASCApplyGameplayEffectToSelfRemoveActiveEffects函数。

可以如下添加Duration GEInfinite GE的Add/Remove事件监听:

AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);

AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);

virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);

virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

Modifiers可以改变Attribute。一个GE 可以有零个或多个Modifiers。每个Modifier负责通过特定的操作去改变它对应的那个Attribute。这些操作包含AddMultiplyDivideOverrideCurrentValue就是将BaseValue通过所有的Modifiers聚合之后的结果。默认情况下,同一类的Modifier先累加,再通过公式聚合,聚合公式为((InlineBaseValue + Additive) * Multiplicitive) / DivisionOverride会直接覆盖上一个Modifier的结果。

有四种Modifiers,它们都能生成一个float类型的值,用于改变AttributeScalable Float可以通过读表,将技能等级映射到一个float值;Attribute Based可以取GESource(创建者)或者Target(目标)的某个属性的CurrentValueBaseValueCustom Calculation Class是通过自定义的办法生成float,是最自由的一种方法;SetByCaller,由GE的创建者提供值的一种方法。

GE会捕获source ASC(创建时)和target ASC(应用时)的GameplayTagsModifiers也可以根据这些tag决定要不要apply。

GE默认情况下是创建新的GameplayEffectSpec实例。但是也可以选择堆叠的形式,改变现有的GameplayEffectSpec的堆叠层数,而不创造新实例。堆叠只对Duration GEInfinite GE生效。有两种堆叠类型,Aggregate by Source会为每一个Source创造一个堆叠实例,各自最多叠加X层;Aggregate by Target则是所有Source共享一个堆叠实例,一起最多叠X层。

Duration GEInfinite GE可以赋予新的GameplayAbilitiesASC上。常见的用法是,例如当你想强制另一个玩家受到击退或者拉近效果,可以添加一个GE,赋予他们一个自动激活的GameplayAbilities,达到强制位移效果。一个GE可以控制赋予何种能力、赋予能力的等级,以及GETarget移除时赋予的能力的处理。有三种处理方式:立即移除能力;允许能力结束后再移除;不再控制能力,交予Target控制。

GE有几种tag分类,每一类都有对应的一个tag容器,用于不同的目的。Gameplay Effect Asset Tags不做任何事,只是用来描述GE的;Granted Tags会在GE添加到ASC时,给它添加容器里的所有tag,在GE移除时移除这些tag;Ongoing Tag Requirements只有当里面所有tag都满足时,GE才会激活,否则会失效,待到条件满足会重新激活;Remove Gameplay Effects with TagsGE被赋予到ASC时,会移除ASC上所有Granted TagsAsset Tags里包含这个目录里tag的GE

GESpecGE的实例化,其中包括了指向对应GE的指针、创建时的等级、创建它的ASC等等。GESpec不一定是在创建之后立马被赋予到Target上,例如一个投射物可以被赋予一个GESpec,当击中物体时再向目标赋予这个GESpec。一个GESpec包含的内容:

  • 创建它的GE的引用
  • 它的等级,通常与GE相同,但也可以不同
  • 它的持续时间,通常与GE相同,但也可以不同
  • 它的periodic effects的发生周期,通常与GE相同,但也可以不同
  • 当前堆叠层数。最大堆叠层数在GE类上
  • GameplayEffectContextHandle,告诉我们谁创建的GESpec
  • GESpec创建时的属性的快照
  • SetByCaller TMaps,保存由外部传入的Map(Tag映射到float),再用于Modifiers或者别的用途

ModifierMagnitudeCalculations (MMC) 是GE中一种功能强大的Modifier。它的能力比Execution弱,但是好处在于可以预测。它可以捕获SourceTarget的属性(可以是快照版本也可以是实时版本),可以获取SetByCallers的数据,并用这些数据计算属性的CurrentValue

Gameplay Effect Execution CalculationExecution)是GE改变ASC最强大的一种方式。它也能像MMC一样捕获属性,但它可以改变ASC的任意属性,不仅仅只是某个属性。因此程序员几乎能用它完成任意想要的功能。权衡处就在于灵活性高,但是不可预测了。

CustomApplicationRequirement (CAR)是可以用来自定义GE应用规则的类。默认的GE应用限制只有GameTag的限制。例如需要新增一种当目标身上某些GE的堆叠层数达到N层才激活的GE,可以从这里扩展。

GameplayAbilitiesGA)可以通过特定的GE来确定激活时的资源消耗。这个GE需要是Instant GE,有一个或多个Modifiers来计算资源Attribute的消耗值。

GA也可以通过特定的GE来实现能力的冷却。这个Cooldown GE需要是一个没有Modifiers,并且对于每一种能力(如果游戏支持能力切换,并且能力槽共享cd,则是能力槽)在GrantedTags中有一个特殊的GameTagCooldown Tag)的Duration GEGA正是通过检查Cooldown Tag的存在而不是Cooldown GE的存在来确定是否能力正在冷却的。

2.5 Gameplay Abilities

GameplayAbilities (GA) 是游戏里Actor可以使用的技能。同一时间可以同时激活多个GA,例如冲刺的同时可以射击。常见的GA:跳跃、冲刺、设计、每X秒免疫一次伤害、使用药水、打开一扇门、收集资源、建造建筑。GA具有等级,可以通过改变GA等级改变GA的数值或功能。GA使用AbilityTasks处理持续一段时间的动作,例如监听某个事件、等待属性改变、等待玩家选择目标等等。 一个简单点的GA流程如下: 一个复杂一些的GA流程如下: 更复杂的GA可以由使用不同的GA进行组合、交互来实现

GA的升级有两种,一种是移除原来的GA的实例,再重新赋予新的等级的GA;一种是直接改原来GA实例的等级。二者流程上有点区别,取决于需求。

2.6 Ability Tasks

GA只在一帧里执行,当我们想要响应一段时间内的事件时,我们需要AbilityTasks。 例如:

  • 移动Character
  • 播放timeline
  • 响应Attribute
  • 响应用户输入
  • and more

例如一种多重护甲技能,当受到伤害减少一层护甲层数,就需要新增AbilityTasks去监听Actor受伤的事件。像击退、冲刺、拉取这样的是用一些影响移动组件的AbilityTasks来实现的

2.7 Gameplay Cues

GameplayCues (GC) 执行的是与gameplay无关的逻辑,例如声效、粒子特效、相机震动等。 GameplayCues一般是从远程同步的(除非是特意由本地管理的)。

3. 常见能力的实现

3.1 眩晕

眩晕时,我们一般取消Character的所有主动类型的GameplayAbilities,阻止新的主动GameplayAbility的激活,并且在眩晕的持续过程中,阻止角色移动。

对于取消GameplayAbilities,我们可以在眩晕时调用ASCAbilitySystemComponent->CancelAbilities(),再添加眩晕的GameTag。为了阻止能力在眩晕时激活,我们可以在阻止这些能力激活的GameTag类别里添加眩晕的GameTag。为了阻止移动,我们可以在角色移动组件的GetMaxSpeed()里当存在眩晕标签时返回0。

3.2 吸血

可以在伤害的Execute流程里计算吸血。如果GEGameTag存在Effect.CanLifesteal,那么Execution会计算吸血量,构造一个Instant GE,将这个值赋予到SourceASC上。