Gameplay-Ability-System

8.1k words

简介

记录GAS中各个模块的使用经验

引用

【gameplay-ability-system-for-unity】
【Unreal】虚幻GAS系统快速入门 - LunarMaxim的文章 - 知乎
UE-GAS架构分析(二)(GameplayAttribute) - 李嘉的文章 - 知乎
UE GAS进阶-深入理解GE - 高贵纯合子的文章 - 知乎

GameplayTag

GameplayTags是一种层级标签,如Parent.Child.GrandChild。替代了原来的Bool,或Enum的结构,可以在玩法设计中更高效 的标记对象的行为或状态。

角色本身的枚举(敌我阵营、职业)、状态(无敌、控制)都可以用Tag存储

设计上:Ability(Atk、Heal、Fire、Bomb)、Effect(Atk、Heal)、GameplayCue(DieVFX、CameraShake、UpdeCD)、Event、Character(Agent、Monster、Neutrality)、Cooldown(Atk、Skill)等基础标签。对应的模块只检测对应的tag。避免重复和模糊定义,反面例子(比如将Attack的GA标签和GE标签、角色处于攻击状态的标签都设置为Character.Attack,不利于追述Tag的来源。)

实现细节

Gameplay Ability System

主要包含的内容

  • ASC(AbilitySystemComponent)
  • GA(GameplayAbilites)
    • 角色的技能,攻击、疾跑、自爆、死亡等
  • AS(AttributeSet)
    • 角色身上可以用float表示的属性(hp、atk、speed)
  • GE(GameplayEffect)
    • 修改属性(hp、atk、speed)
  • GC(GameplayCues)
    • 播放特效
    • 播放音效

AbilitySystemComponent

Ability System Component(ASC)是整个GAS的基础组件。
ASC本质上是一个UActorComponent,用于处理整个框架下的交互逻辑,包括使用技能 (GameplayAbility)、包含属性(AttributeSet)、处理各种效果(GameplayEffect)。
所有需要应用GAS的对象(Actor),都必须拥有GAS组件。
拥有ASC的Actor被称为ASC的OwnerActor,ASC实际作用的Actor叫做AvatarActor。ASC可以被赋予某个角色ASC,也可以被赋予PlayerState(可以保存死亡角色的一些数据

如果Character需要销毁再重新生成,如MOBA游戏角色死亡后泉水复活,那么ASC可以放在PlayerState上避免随着角色一同销毁。此时的OwnerActor是PlayerState,AvatarActor则是Character。

实现细节

GameplayAbility

Gameplay Ability(GA)标识了游戏中一个对象(Actor)可以做的行为或技能。 能力(Ability)可以是普通攻击或者吟唱技能,可以是角色被击飞倒地,还可以是使用 某种道具,交互某个物件,甚至跳跃、飞行等角色行为也可以是Ability。
Ability可以被赋予对象或从对象的ASC中移除,对象同时可以激活多个GameplayAbility。 *基本的移动输入、UI交互行为则不能或不建议通过GA来实现

Image text

需要拥有GA后,才能使用GA(通过给对象身上挂在AbilitySystemComponent,数据中保存拥有多少的Abilitity)

使用分为实例化释放两个过程,前者主要是生成一个 GameplayAbilitySpec 对象(抽象类,定义 AbilitySpec CreateSpec 的方法),并作为一部份非共有(非静态)属性赋值。

GameplayAbility的主要功能

  • 设置 GameplayAbility 的 Tag、CD、Cost等属性
  • 获取必要信息
  • 编写逻辑,比如播放动画、应用 GameplayEffect、应用冲量?等
  • 需要执行 EndAbility 结束

添加Ability的手段

  • 配置时,在 AbilitySystemComponentPreset 的配置中添加对应的 Ability
  • 代码动态添加,asc.GrantAbility(ability);
  • 使用 GameplayEffect 添加 GameplayAbility,对于非即时的 GameplayEffect,在 GrantAbility 项添加对应的 GameplayAbility。

调用Ability的方式

  • 主动释放(释放技能)
    • 通过 ByClass 的方式:一次只能 Activate 一个 GA
    • 通过 ByTag 的方式:可以 Activate 任意多个 GA,配合 Tag 容器使用
  • 被动释放(受击)

Ability的触发条件

标签

可以限制各种技能的相互关系,比如收击时不能翻滚(设计时需要把受击时不能释放的技能都放在同一父层级下)。Tag建议以 Ability 开头。

  • AbilityTags:GA的标签
  • CancelAbilityWithTags:激活该GA时,打断其他拥有所有所选标签的GA
  • BlockAbilityWithTags:激活该GA时,阻止激活拥有所选标签的GA
  • ActivationOwnedTags:激活该GA时,赋予ASC所选的GA
  • ActivationRequiredTags:激活GA时,ASC需要的标签
  • ActivationBlockedTags:激活GA时,ASC不能有的标签
    (SourceRequiredTags\SourceBlockedTags\TargetRequiredTags\TargetBlockedTags未实现)
冷却和消耗

需要添加冷却和消耗,则需要写好对应的GE,在配置时,配置好对应的数据。在 Cooldown GE 持续期间,玩家的ASC组件会携带对应技能的 CooldownTag,本质是通过 Tag 来限制的。对于每一个 GA 都需要写一遍 Cost 和 CD 的 GE。

【优化方式】,原理为在实例化生成 GE Spec时,修改其 Cost 和 Cooldown 属性后再将其应用。

  1. 使用MMC。这是最简单的方法。创建一个MMC,从实例中读取成本值,GameplayAbility该实例可以从 中获取GameplayEffectSpec。(EX-GAS已实现)
1
2
3
4
5
6
7
8
9
10
11
float UPGMMC_HeroAbilityCost::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(Spec.GetContext().GetAbilityInstance_NotReplicated());

if (!Ability)
{
return 0.0f;
}

return Ability->Cost.GetValueAtLevel(Ability->GetAbilityLevel());
}
  1. 覆盖UGameplayAbility::GetCostGameplayEffect()。覆盖此函数并在运行时创建一个GameplayEffect,用于读取上的成本值GameplayAbility。(AbstractAbility 的 SetCooldown、SetCost)
冷却GE的要求
  • Duration 类型
  • GrantedTags 为技能的冷却 Tag,如 Cooldown.Skill1
消耗GE的要求
  • Instant 类型
  • 有一个或多个 Modifier 来修改对应的属性

AbilityTask

GA 是一帧内完成的,如果想要实现类似的 Wait 的异步逻辑需要使用 Task。EX-GAS 中有Timeline 配置项。

Image Text

实现细节

AttributeSet

AttributeSet 负责定义和持有属性,并且管理属性的变化,包括网络同步。 需要在Actor中被添加为成员变量,并注册到ASC(C++)。
一个ASC可以拥有一个或多个(不同的)AttributeSet,因此可以角色共享一个很大的 Attribute Set,也可以每个角色按需添加Attribute Set。可以在属性变化前(PreAttributeChange)后(PostGameplayEffectExecute)处理相关 逻辑,可以通过委托的方式绑定属性变化。

BaseValue跟CurrentValue

AttributeValue 由两个float(BaseValue、CurrentValue)构成,BaseValue是属性的基础值,而CurrentValue是由于某些Buff加成变动后的值。当Buff褪去后,就可以回到BaseValue。所以分了俩个属性。比如玩家当前HP的值是100,加了一个提升20%血量的Buff。则HP的BaseValue为100,CurrentValue为120。Buff褪去后,CurrentValue变回BaseValue的值:100

实现细节

分別由哪种类型的GE改变?

  • BaseValue由Instant(立即)的GE改变.更新会导致 CurrentValue的重新计算(根据 CalculateMode(叠加、最大值、最小值) 计算)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    public void ApplyModFromInstantGameplayEffect(GameplayEffectSpec spec)
    {
    foreach (var modifier in spec.Modifiers)
    {
    ...

    AttributeSetContainer.Sets[modifier.AttributeSetName]
    .ChangeAttributeBase(modifier.AttributeShortName, baseValue);
    }
    }
  • CurrentValue由Duration(持续)、Infinite(永恒) 的GE改变。

    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

    /// <summary>
    /// it's triggered only when the owner's gameplay effect is added or removed.
    /// </summary>
    void RefreshModifierCache()
    {
    // 注销属性变化监听回调
    UnregisterAttributeChangedListen();
    _modifierCache.Clear();
    var gameplayEffects = _owner.GameplayEffectContainer.GameplayEffects();
    foreach (var geSpec in gameplayEffects)
    {
    if (geSpec.IsActive)
    {
    foreach (var modifier in geSpec.Modifiers)
    {
    if (modifier.AttributeName == _processedAttribute.Name)
    {
    _modifierCache.Add(new Tuple<GameplayEffectSpec, GameplayEffectModifier>(geSpec, modifier));
    TryRegisterAttributeChangedListen(geSpec, modifier);
    }
    }
    }
    }

    UpdateCurrentValueWhenModifierIsDirty();
    }

GameplayEffect

Gameplay Effect(GE)是Ability对自己或他人产生影响的途径。 GE通常可以被理解为我们游戏中的buff。比如增益/减益效果(修改属性)。
但是GAS中的GE也更加广义,释放技能时候的伤害结算,施加特殊效果的控制、霸体效果(修改GameplayTag)都是通过GE来实现的。
GE相当于一个可配置的数据表,不可以添加逻辑。开发者创建一个UGameplayEffect的派生蓝图,就可以根据需求制作想要的效果。

GE是修改Attribute的唯一渠道!

Stacking

用于叠加多个GE的效果,仅能用于 Infinite 和 Duration 的GE。

  • StackingType:叠加栈在目标身上or施法者身上(ByTarget时,3个敌人最多对我释放3层、BySource时,3个敌人可以对我叠加9层)
  • 持续时间刷新策略:
    • 不刷新
    • 每次apply成功后刷新持续时间
  • 周期重制策略:
    • 不重制
    • 每次apply成功后重置Effect的周期计时
  • 持续时间结束策略:
    • 持续时间结束时,清除所有层数
    • 持续时间结束时减少一层,然后重新经历一个Duration
    • 持续时间结束时,再次刷新Duration,相当于无限Duration

Overflow

设置Stack溢出会Apply的GE。

应用

从GA或者ASC去Apply一个GE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

[SerializeField]
private GameplayEffectAsset _geBulletDamageAsset;
private GameplayEffect _geBulletDamage;
_geBulletDamage = new GameplayEffect(_geBulletDamageAsset);

if(_asc && other.gameObject.TryGetComponent(out AbilitySystemComponent enemy)){
if (enemy.HasTag(GTagLib.Faction_Enemy))
{
_asc.ApplyGameplayEffectTo(_geBulletDamage, enemy);
//TODO 对象池回收
GameObject.Destroy(gameObject);
}
}

GameplayEffect的施加(Apply)和 激活(Activate)

  • GameplayEffect的施加(Apply)和激活(Activate)是两个概念,施加是指GameplayEffect被添加到目标身上,激活是指GameplayEffect实际生效。
    • 为什么做区分?
    • 举个例子:固有被动技能(Ability)是持续回血,被动技能的逻辑显然是永久激活的状态,而持续回血的效果(GameplayEffect) 来源于被动技能,那如果单位受到了外部的debuff禁止所有的回血效果,那么是不是被动技能被禁止?显然不是,被动技能还是会持续激活的。 那应该是移除回血效果吗?显然也不是,被动技能整个过程是不做任何变化,如果移除回血效果,那debuff一旦消失,谁再把回血效果加回来? 所以,这里需要区分施加和激活,被动技能的持续回血效果被施加到单位身上,而debuff做的是让回血效果失活,而不是移除回血效果,一旦debuff结束, 回血效果又被激活,而这个激活的操作可以理解为回血效果自己激活的(依赖于Tag系统)。

实现细节

GameplayCue

GameplayCues (GC) 执行非游戏性相关的事情,比如音效,粒子特效,震屏等。GameplayCues通常会被复制和预测(除非设置Executed, Added或Removed是本地的)。分为 Instant 和 Duration 两种(GameplayCueInstant、GameplayCueDurational)。

调用

一般通过GE配置,也可以在GA里配置

实现细节

ModifierMagnitudeCalculation

数值计算公式,唯一的使用场景是在GameplayEffect中。 GAS中,体系内运作的情况下,只有GameplayEffect才能修改Attribute的数值。而GameplayEffect就是通过MMC修改Attribute的数值。

  • ScalableFloatModCalculation:可缩放浮点数计算

    • 该类型是根据Magnitude计算Modifier模值的,计算公式为:ModifierMagnitude * k + b 实际上就是一个线性函数,k和b为可编辑参数,可以在编辑器中设置。
  • AttributeBasedModCalculation:基于属性的计算

    • 该类型是根据属性值计算Modifier模值的,计算公式为:AttributeValue * k + b 计算逻辑与ScalableFloatModCalculation一致。
    • 重点在于属性值的来源,确定属性值来源的参数有3个:
      • attributeFromType:属性值从谁身上取?是从游戏效果的来源(创建者),还是目标(拥有者)。
      • attributeName:属性值的名称,比如战斗属性集里的生命值:AS_Fight.Health
      • captureType:属性值的捕获类型
      • Track: 追踪,在Modifier被执行时,当场去取属性值
      • SnapShot: 快照,在游戏效果被创建时会对来源和目标的属性进行快照。在Modifier被执行时,去取快照的属性值。
  • SetByCallerModCalculation:由调用者设置的计算

    • 不使用任何值计算模值,而是在执行时由调用者给出Modifier模值。

    • 通过对GameplayEffectSpec注册数值来实现设置值。

    • 设置数值映射有2种:

      • 自定义键值:通过GameplayEffectSpec的RegisterValue(string key, float value)
      • GameplayTag:通过GameplayEffectSpec的RegisterValue(GameplayTag tag, float value)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

      [SerializeField] private string valueName;
      public override float CalculateMagnitude(GameplayEffectSpec spec,float input)
      {
      var value = spec.GetMapValue(valueName);
      return value ?? 0;
      }

      [SerializeField]
      [ValueDropdown("@ValueDropdownHelper.GameplayTagChoices", HideChildProperties = true)]
      private GameplayTag _tag;

      public override float CalculateMagnitude(GameplayEffectSpec spec, float input)
      {
      var value = spec.GetMapValue(_tag);
      return value ?? 0;
      }

  • CustomCalculation:自定义计算(必须继承自抽象基类ModifierMagnitudeCalculation)

    • 上述3种类型显然不够方便且全面的满足游戏开发者的所有需求,所以提供了自定义计算类的功能。
    • 允许开发者自由发挥给出各种各样的计算逻辑。

总结

  • ASC管理GA、GE、Attribute。
  • GE可以用来给予ASC一个GA,也可以修改Attribute。(甚至还能Apply其他的GE,图中没有提到)
  • GA可以发送Event给其他ASC,调用对应的GA;也可以对目标Apply一个GE,修改其属性。
  • GE和GA都可以用来触发GC。
    Image text