Skip to content

No78Vino/gameplay-ability-system-for-unity

Repository files navigation

EX Gameplay Ability System For Unity

前言

该项目为Unreal Engine的Gameplay Ability System的Unity实现,目前实现了部分功能,后续会继续完善。

该项目完全开源,欢迎大家一起参与开发,提出建议,共同完善。可以基于该项目进行二次开发。

!!提醒!!请注意!!

该项目依赖Odin Inspector插件(付费),请自行解决!!!!!!!!

Odin Inspector插件请使用3.2+版本

若没有方法解决,可以加反馈qq群(616570103),群内提供帮助

目前EX-GAS没有非常全面的测试,所以存在不可知的大量bug和性能问题。所以对于打算使用该插件的朋友请谨慎考虑,当然我是希望更多人用EX-GAS,毕竟相当于是变相的QA。

但是要讲良心嘛,现在的EX-GAS算不上很稳定的版本,如果你是业余时间开发rpg类的独立游戏,开发时间十分充裕,我当然建议你试试EX-GAS,我会尽可能修复bug,提供使用上的帮助。

如果有好兄弟确实打算用,那么请务必加反馈群(616570103)。我会尽力抽时间修复提出的bug。

我非常希望EX-GAS能早日稳定,为更多游戏提供支持帮助。

入门教学案例系列文章

俯视角2D弹幕射击游戏

目录

1.快速开始

安装

  1. 导入Odin Inspector插件(付费),Odin Inspector来源请自行解决。建议使用3.2+版本。
  2. 导入本插件,建议以下两种方式:

【国内镜像】https://gitee.com/exhard/gameplay-ability-system-for-unity.git?path=Assets/GAS

  • 使用git clone本仓库[镜像同上],然后将Assets/GAS文件夹拷贝到你的项目中即可

使用

GAS十分复杂,使用门槛较高。因为本项目是对UE的GAS的模仿移植,所以实现逻辑基本一致。建议先粗略了解一下UE版本的GAS整体逻辑,参考项目文档:https://github.com/BillEliot/GASDocumentation_Chinese

使用流程

  1. 基础设置

在ProjectSetting中(或者Edit Menu栏入口:EX-GAS -> Setting),找到EX Gameplay Ability System的基本设置界面:

S0X2@(E97LP_SWIJY2SJ@F3.png

设置好以下两个路径

  • 配置文件Asset路径: 这是该项目有关GAS的配置的路径,包括MMC,Cue,GameplayEffect,Ability,ASCPreset。
  • 脚本生成路径: GAS的基础配置(Tag,Attribute,AttributeSet)都会有对应的脚本生成。

首次设置完路径后,点击检查子目录文件夹,确保必要的子文件夹都已生成。

【生成AbilitySystemComponentExtension类脚本】这个按钮,请在生成了Attribute,AttributeSet,Ability的Lib集合类之后再点击。特别提醒,AbilitySystemComponentExtension是工具类,理论上只生成一次即可。

  1. 配置Tag: Tag是GAS核心逻辑运作的依赖,非常重要。关于Tag的使用及运作逻辑详见章节(GameplayTag)

  2. 配置Attribute: Attribute是GAS运行时数据单位。关于Attribute的使用及运作逻辑详见章节(Attribute)

  3. 配置AttributeSet: AttributeSet是GAS运行时数据单位集合,合理的AttributeSet设计能够帮助程序解耦,提高开发效率。关于AttributeSet的使用及运作逻辑详见章节(AttributeSet)

  4. 设计MMC,Cue: 详见MMC, GameplayCue

  5. 设计Gameplay Effect: 详见 Gameplay Effect

  6. 设计Ability: 详见 Ability

  7. 设计ASC预设(可选): 详见 AbilitySystemComponent

GAS的预缓存

  • 为了解决EX-GAS框架中关于Type.Name造成的GC问题。 可以在游戏内合适的地方(一般是游戏初始化阶段)调用GasCache.CacheAttributeSetName(GAttrSetLib.TypeToName) 这行代码。他会将GAttrSetLib.TypeToName中的所有Key-Value对缓存起来,避开Type.Name的使用。 如果你不调用这个方法,也不会对运行逻辑造成影响,不过会有额外的GC。

2.EX-GAS系统介绍

2.1 EX-GAS概述

EX-GAS是对UnrealEngine的GAS(Gameplay Ability System)的模仿和实现。

GAS 是 "Gameplay Ability System" 的缩写,是一套游戏能力系统。 这个系统的目的是为开发者提供一种灵活而强大的框架,用于实现和管理游戏中的各种角色能力、技能和效果。

如果把EX-GAS高度概括为一句话,那就是:WHO DO WHAT

  • Who:AbilitySystemComponent(ASC),EX-GAS的实例对象,是体系运转的基础单位
  • Do:Ability,是游戏中可以触发的一切行为和技能
  • What:GameplayEffect(GE),掌握了游戏内元素的属性实际控制权,GameplayEffect本身应该理解为结果

GAS本质是一套属性数值的管理系统,GameplayCue我个人理解为附加价值(虽然这个附加价值很有分量)。 纵使GAS的Tag体系解决复杂的GameplayEffect和Ability的逻辑,但最终的结果目的也只是掌握属性数值变化。 而属性的最底层修改权力交由了GameplayEffect。所以我把GE理解为结果。

UE的GAS的使用门槛很高,这一点在我构筑完EX-GAS雏形后更是深有体会。 所以在EX-GAS的设计上,我尽可能的做简化,优化,来降低了使用门槛。 我制作了几个关键的编辑器,来帮助开发者快速的使用EX-GAS。 但即便如此,GAS本身的繁多参数依然让编辑器的界面看上去十分臃肿,这很难简化,没有哪个参数是可以被删除的。 甚至,雏形阶段的EX-GAS还有很多功能还未实现,也就是说还有更多的参数是没有被编辑器暴露出来的。

GAS的使用者必须至少有一名程序开发人员,因为GAS的使用需要编写大量自定义业务逻辑。 Ability,Cue,MMC等都是必须根据游戏类型和内容玩法而定的。 非程序开发人员则需要完全理解EX-GAS的运作逻辑,才能更好的配合程序开发人员快速配置出各种各样的技能,完善玩法表现。

2.2 GameplayTag

Gameplay Tag,标签,它用于分类和描述对象的状态,非常有用于控制游戏逻辑。

  • Gameplay Tag以树形层级结构(如Parent.Child.Grandchild)组织,用于描述角色状态/事件/属性/等,如眩晕(State.Debuff.Stun)。
  • Gameplay Tag主要用于替代布尔值或枚举值,进行逻辑判断。
  • 通常将Gameplay Tag添加到AbilitySystemComponent(ASC)以与系统(GameplayEffect,GameplayCue,Ability)交互。

Gameplay Tag在GAS中的使用涉及到标签的添加、移除以及对标签变化的响应。 开发者可以通过GameplayTag Manager在项目设置中管理这些标签,无需手动编辑配置文件。 Gameplay Tag的灵活性和高效性使其成为GAS中控制游戏逻辑的重要工具。 它不仅可以用于简单的状态描述,还可以用于复杂的游戏逻辑和事件触发。

举个例子,GameplayEffect中有一个字段RequiredTags,其含义是当前GameplayEffect生效的AbilitySystemComponent(ASC) 需要拥有【所有】的RequiredTags(需求标签)。

上述例子,如果用传统的思路去做,可能需要写很多if-else判断,同时元素的实例脚本可能会增加很多状态标记的变量, 而且还需要考虑多个游戏效果的交互,这使得代码的设计和实现变得复杂,耦合。

GameplayTag的使用可以大大简化这些逻辑,使得代码更加清晰,易于维护。 他把状态和标记全部抽象成了一个独立的Tag系统,而且最巧妙的是树形结构的设计。 他解决了很多Gameplay设计上的问题,常见的问题比如:移除所有Debuff,传统的做法可能是让(中毒,减速,灼伤,等等)继承自Debuff类/接口; 而GameplayTag只需要添加一个Tag(中毒:Debuff.Poison,减速:Debuff.SpeedDown,灼伤:Debuff.Burning)

GameplayTag自身可以作为一个独立的系统去使用。 我在开发Demo的过程中就发现了GameplayTag的强大之处,他几乎替代了我的所有状态值。 甚至我设计了一个全局ASC,专门用来管理全局状态,我不需要对每个系统的状态管理,转而维护一个ASC即可。(虽然最后并没有落地这个设计,因为DEMO没有那么复杂。)

2.2.a GameplayTag Manager

QQ20240313114652.png 我模仿了UE的GAS的Tag管理视图,做了树结构管理。通过选中节点,可以添加子节点,删除节点。要修改父子级关系时,只需要拖动节点即可。 Tag的分类设计需要谨慎,策划(设计师)需要再项目初就规划好Tag的大致分类。如果开发后期出现了Tag的父子级关系大变动,会严重影响原有游戏运作逻辑。

【注意!!!】 每次编辑完Tag后,一定要点击右上角的【生成TagLib】按钮。GTagLib是包含了当前所有Tag的库类, 便于程序开发使用。GTagLib中Tag变量名会用‘_’代替‘.’ ,比如 State.Buff.PowerUp -> State_Buff_PowerUp

2.3 Attribute

Attribute,属性,是GAS中的核心数据单位,用于描述角色的各种属性,如生命值,攻击力,防御力等。

Attribute和AttributeSet(属性集)需要结合起来才能作为唯一标识,简单点说AttributeSet是姓氏,Attribute是名字。 不同的AttributeSet可以有相同名字的Attribute,但是同一组的AttributeSet不可以有相同名字的Attribute。 常见的情况,如下:

AttributeSet 人物: 生命值, 法力, 攻击力, 防御力

AttributeSet 武器: 生命值(耐久度), 攻击力, 防御力

而这两组AttributeSet中的生命值,攻击力,防御力,都是不同的属性,他们的意义和作用不同。但他们可以属于同一个单位。

2.3.a Attribute Manager

QQ20240313115953.png Attribute Manager的作用很简单,提供属性名字的管理。然后作为AttributeSet的选项使用。

【注意!!!】 每次编辑完Attribute后,一定要点击【生成AttrLib】按钮。 只有AttrLib生成后,AttributeSet的Attribute选项才会发生改变。

2.4 AttributeSet

AttributeSet,属性集,是GAS中的核心数据单位集合,用于描述角色的某一类别的属性集合。

在上文的[2.3 Attribute]中,我们提到了AttributeSet是姓氏,Attribute是名字。二者结合起来才能作为唯一标识。 而对于AttributeSet的设计,可以较为随意,大多数情况,大家会更乐意一个单位只有一个AttributeSet。 因为这样便于管理和分类,不同类别的单位直接使用不同的AttributeSet。但实际上一个单位是可以拥有复数AttributeSet。 我其实比较认同一个单位只有一个AttributeSet的设计,因为这对程序开发也是好事,逻辑处理会更简单直白。

配置时的注意项:

  • AttributeSet的名字禁止重复或空。这是因为AttributeSet的名字会作为类名。
  • AttributeSet内的Attribute禁止重复。

2.4.a AttributeSet Manager

QQ20240313121300.png AttributeSet Manager统筹属性集的命名和属性管理。

【注意!!!】 每次编辑完后,一定要点击【生成AttrSetLib】按钮。AttrSetLib会在AbilitySystemComponent的预设配置中用到。 AttrSetLib.gen.cs脚本中会包含所有的AttributeSet类(详见下文API章节),AttributeSet对应的类名是:“AS_名字”。 比如AttributeSet名字是Fight,那它对应的类名是AS_Fight。

2.5 ModifierMagnitudeCalculation

ModifierMagnitudeCalculation,修改器,负责GAS中Attribute的数值计算逻辑。

MMC(下文开始会使用缩写指代ModifierMagnitudeCalculation)唯一的使用场景是在GameplayEffect中。 GAS中,体系内运作的情况下,只有GameplayEffect才能修改Attribute的数值。而GameplayEffect就是通过MMC修改Attribute的数值。

MMC具有以下特点:

  • 【与Attribute集成】: MMC 与 GAS 中的Attribute系统一起使用。这意味着计算效果幅度时可以考虑角色的属性,使得效果的强度与角色的属性值相关联。
  • 【实时性】: MMC 用于在运行时动态计算 Attribute 的修改幅度。这样可以根据角色的状态、属性或其他因素,实时地调整效果的强度。
  • 【自定义】: 通过继承 MMC的基类,开发者可以实现自定义的计算逻辑。这允许在计算效果幅度时考虑复杂的游戏逻辑、属性关系或其他条件。
  • 【复用性】: 由于 MMC 是一个独立的类(Scriptable Object),开发者可以在多个 GameplayEffect 中重复使用相同的计算逻辑。这样可以确保在整个游戏中保持一致的效果计算。
  • 【灵活性】: 使用 MMC 提高了系统的灵活性,使得效果的强度不再是固定的数值,而可以根据需要在运行时进行调整,适应不同的游戏情境和需求。

MMC在GameplayEffect中的运作逻辑,结合GameplayEffect配置中MMC界面来解释。如下图:

QQ20240313145154.png

MMC被存储在Modifier中,Modifier是GameplayEffect的一部分。 Modifier包含了修改的属性,幅值(Magnitude),操作类型和MMC。

  • 修改的属性:指的是GameplayEffect作用对象被修改的属性。可以看到属性名是“AS_Fight.POSTURE”,这对应了上文的提到的属性识别是AttrSet和Attr组合而成的。
  • 幅值Magnitude:修改器的基础数值。这个数值如何使用由MMC的运行逻辑决定。
  • 操作类型:是对属性的操作类型,有3种:
    • Add : 加法(取值为负便是减法)
    • Multiply: 乘法(除法取倒数即可)
    • Override:覆写属性值
  • MMC:计算单位,Modifier的核心,是一个ScriptableObject。MMC的类别如下:
    • 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)
    • CustomCalculation:自定义计算(必须继承自抽象基类ModifierMagnitudeCalculation)
      • 上述3种类型显然不够方便且全面的满足游戏开发者的所有需求,所以提供了自定义计算类的功能。
      • 允许开发者自由发挥给出各种各样的计算逻辑。

2.6 GameplayCue

目前EX-GAS的GameplayCue功能还未完善。功能相对简陋。

  • 【GameplayCue的作用】:GameplayCue是一个用于播放游戏提示的类,它的作用是在游戏运行时播放游戏效果,比如播放一个特效、播放一个音效等。
  • 【GameplayCue的原则】: Cue是游戏提示,他必须遵守以下原则:
    • Cue不应该对游戏的数值体系产生影响,比如不应该对游戏的属性进行修改,不应该对游戏的Buff进行修改等。
    • Cue不应该对游戏玩法产生实际影响,比如即时战斗类的游戏,Cue不应该影响角色的位移、攻击等。

第一条原则是所有类型游戏必须遵守的。

而第二条原则就见仁见智了,因为游戏类型和玩法决定了cue的影响范围。 比如即时战斗类游戏,cue对角色位移有操作显然就是干涉了战斗,但如果是回合制游戏,cue对角色位移的操作就可以被当成是动画表现。 (甚至,即便是即时战斗类游戏,cue对角色位移的操作也可以被当成是动画表现,只要游戏开发人员认为cue的位移操作不影响游戏的战斗结果即可。)

  • 【GameplayCue的类型】 GameplayCue的类型分两大类:
    • GameplayCueInstant:瞬时性的Cue,比如播放动画,伤害UI提示等
    • GameplayCueDurational:持续性的Cue,比如持续性的特效、持续性的音效等

GameplayCueInstant和GameplayCueDurational都是抽象类,它们的子类才是真正的可使用Cue类。 Cue是需要程序开发人员大量实现的,毕竟游戏不同导致游戏提示千变万化。

  • 【关于Cue的子类实现】: Cue的完整组成为GameplayCue和GameplayCueSpec:

    • GameplayCue< T >(抽象基类,T为对应的Spec类):Cue的数据实类,是一个可编辑类,开发人员可以在编辑器中设置Cue的各种参数。该类只可以被视作数据类。
      • 必须实现CreateSpec方法:用于创建对应的Spec类
    • GameplayCueSpec(抽象基类):Cue的规格类,是Runtime下Cue的真正实例,Cue的具体逻辑在该类中实现。
      • Spec类 需要实现的方法 方法触发时机
        GameplayCueInstantSpec
        瞬时性Cue的规格类
        Trigger() 执行时触发
        GameplayCueDurationalSpec
        持续性Cue的规格类
        OnAdd() Cue被添加时触发
        GameplayCueDurationalSpec OnRemove() Cue被移除时触发
        GameplayCueDurationalSpec OnGameplayEffectActivated() Cue所属的GameplayEffect被激活时触发
        GameplayCueDurationalSpec OnGameplayEffectDeactivated() Cue所属的GameplayEffect被移除时触发
        GameplayCueDurationalSpec OnTick() Cue的每帧更新逻辑
  • 【关于Cue的参数传递】: 目前EX-GAS的Cue参数传递非常简陋,依赖于结构体GameplayCueParameters,成员如下:

    • GameplayEffectSpec sourceGameplayEffectSpec:Cue所属的GameplayEffect实例(如果是GE触发)
    • AbilitySpec sourceAbilitySpec:Cue所属的Ability实例(如果是Ability触发)
    • object[] customArguments:自定义参数,不同于GameplayCue中的数据。 customArguments是供程序开发人员在业务逻辑内自由传递参数的载体。

注意:customArguments是一个object数组,开发人员需要自己保证传递的参数类型正确,否则会导致运行时错误。 customArguments是最暴力的设计,往后EX-GAS的Cue参数传递设计还会进行优化。

  • 【GameplayCue的使用】: GameplayCue的使用手段很多,最基础的是在GameplayEffect中使用,Cue最开始的设计基础也是依附于GameplayEffect。Ability也可以对Cue进行操作。 除此之外,Cue的使用不限制于EX-GAS的体系内。开发者可以在任何地方使用Cue,只要能获取到GameplayCue的资源实例并且遵守Cue的原则即可。
    • 在GameplayEffect中使用Cue

      • GameplayEffect中使用Cue会根据GameplayEffect执行策略产生变化。
        • GameplayEffect类型 Cue名称 Cue类别 执行时机
          Instant CueOnExecute Instant GameplayEffect执行时触发
          Durational CueDurational Durational 生命周期完全和GameplayEffect同步
          Durational CueOnAdd Instant GameplayEffect添加时触发
          Durational CueOnRemove Instant GameplayEffect移除时触发
          Durational CueOnActivate Instant GameplayEffect激活时触发
          Durational CueOnDeactivate Instant GameplayEffect失活时触发
    • 在Ability中使用Cue

      • Ability种Cue的使用完全依赖于Ability自身的业务逻辑,因此程序开发者在AbilitySpec中实现Cue逻辑时一定要保证合理性。 特别是对于Durational类型的Cue,一定要保证Cue生命周期的合理性,切记不要出现遗漏销毁Cue的情况。

2.7 GameplayEffect

GameplayEffect是EX-GAS的核心之一,一切的游戏数值体系交互基于GameplayEffect。

GameplayEffect掌握了游戏内元素的属性控制权。理论上,只有它可以对游戏内元素的属性进行修改 (这里指的是修改,数值的初始化不算是修改)。当然,实际情况下,游戏开发人员当然可以手动直接修改属性值。 但是还是希望游戏开发者尽可能的不要打破EX-GAS的数值体系逻辑,因为过多的额外操作可能会导致游戏的数值体系变得混乱,难以追踪数值变化等等。

另外GameplayEffect还可以触发Cue(游戏提示)完成游戏效果的表现,以及控制获取额外的能力等。

  • GameplayEffect的使用 QQ20240313152015.png

GameplayEffect的配置界面如图,接下来逐一解释各个参数的含义。

  • Name
    • GameplayEffect的名称,纯粹用于显示,不会影响游戏逻辑。方便编辑者区分GameplayEffect。
  • Description
    • GameplayEffect的描述,纯粹用于显示,不会影响游戏逻辑。方便编辑者阅读理解GameplayEffect。
  • DurationPolicy:GameplayEffect的执行策略,有以下几种:
    • 策略类型 执行逻辑
      Instant 即时执行,GameplayEffect被添加时立即执行,执行完毕后销毁自身。
      Duration 持续执行,GameplayEffect被添加时立即执行,持续时间结束后移除自身。
      Infinite 无限执行,GameplayEffect被添加时立即执行,执行完毕后不会移除,需要手动移除。
  • Duration
    • 持续时间,只有DurationPolicy为Duration时有效。
  • Every(Period)
    • 周期,只有DurationPolicy为Duration或者Infinite时有效。每隔Period时间执行一次PeriodExecution。
  • PeriodExecution
    • 周期执行的GameplayEffect,只有DurationPolicy为Duration或者Infinite,且Period>0时有效。每隔Period时间执行一次PeriodExecution。 _PeriodExecution禁止为空!!_PeriodExecution原则上只允许是Instant类型的GameplayEffect。但如果根据开发者需求,也可以使用其他类型的GameplayEffect。
  • GrantedAbilities
    • 授予的能力,只有DurationPolicy为Duration或者Infinite时有效。在GameplayEffect生命周期内,GameplayEffect的持有者会被授予这些能力。 GameplayEffect被移除时,这些能力也会被移除。具体详见GrantedAbility
  • Modifiers: 属性修改器。详见MMC
  • Tags:标签。Tag具有非常重要的作用,合适的tag可以处理GameplayEffect之间复杂的关系。
    • Tag类型 作用
      AssetTags 描述性质的标签,用来描述GameplayEffect的特性表现,比如伤害、治疗、控制等。
      GrantedTags GameplayEffect的持有者会获得这些标签,GameplayEffect被移除时,这些标签也会被移除。Instant类型的GameplayEffect的GrantedTags是无效的。
      ApplicationRequiredTags GameplayEffect的目标单位必须拥有【所有】这些标签,否则GameplayEffect无法被施加到目标身上。
      OngoingRequiredTags GameplayEffect的目标单位必须拥有【所有】这些标签,否则GameplayEffect不会被激活。 (施加和激活是两个概念,如果已经被施加的GameplayEffect持续过程中,目标的tag变化了,不满足,效果就会失活;满足了,就会被激活)。Instant类型的GameplayEffect的OngoingRequiredTags是无效的。
      RemoveGameplayEffectsWithTags GameplayEffect的目标单位当前持有的所有GameplayEffect中,拥有【任意】这些标签的GameplayEffect会被移除。
      Application Immunity Tags GameplayEffect的目标单位拥有【任意】这些标签,就对该GameplayEffect免疫。
    • DurationPolicy为Instant时
      • CueOnExecute(Instant):GameplayEffect执行时触发。
    • DurationPolicy为Duration或者Infinite时
      • CueDurational(Durational):生命周期完全和GameplayEffect同步
      • CueOnAdd(Instant):GameplayEffect添加时触发
      • CueOnRemove(Instant):GameplayEffect移除时触发
      • CueOnActivate(Instant):GameplayEffect激活时触发。
      • CueOnDeactivate(Instant):GameplayEffect失活时触发。
  • Stacking:堆叠。该系列参数是为了处理常见的叠层类型Buff。比如《黑帝斯》中酒神,爱神,冬神的叠攻buff。stacking的参数基本囊括了绝大多数的叠层型buff的设计。
    • 生效的GE类型:只有非Instant类型(持续型)的GameplayEffect,可以产生叠加(stacking)。
    • stackingCodeName: 堆叠GE的唯一标识码,用于可堆叠GE的识别。
      • 本身是字符串,但是runtime实际使用的是其对应的HashCode。如果为空,则视为不可堆叠
      • stackingCodeName除了基础的堆叠类GE识别功能外,另一个作用是用于支持不同GE的共同堆叠。举个例子:有一个团队性质的增伤buff【元素增伤】,团队所有成员对同一个目标都可以叠加【元素增伤】,至多10层,增伤 随层数增加而增加。但是增伤是指定第一个施加buff成员的元素,比如第一层打的是【火增伤】,那么之后不管是【水增伤】,【雷增伤】,都是【火增伤】buff往上叠加。遇到这种特殊情况,就可以把【水增伤】,【雷增伤】,【火增伤】 的stackingCodeName设置为同一个值,这样就可以实现【元素增伤】的共同堆叠。
    • stackingType:GameplayEffect的叠加类型,有三种:
      • stacking类型 作用
        None 不叠加
        AggregateBySource 基于GE来源(ASC)的叠加计数,所有释放单位各自管理一个叠加计数的GE。 举例:BUFF【聚能】效果是单位被叠加三次该buff(来自同一单位)后触发爆炸。 小怪被A玩家叠了2次【聚能】,然后B玩家又对小怪施加了1次【聚能】,但是不会触发爆炸。因为叠加计数是按来源单位各自计数,需要A再叠1次或者B叠2次,小怪才会爆炸。
        AggregateByTarget 基于GE目标(ASC)的叠加计数,所有释放单位共享一个叠加计数的GE。举例:BUFF【诅咒】效果是单位被叠加3次该buff(无关来源单位)后触发即死效果。经典魂游的咒蛙攻击buff。玩家被数只咒蛙围攻,只要被咒蛙打到3次就死亡。在场所有咒蛙的【诅咒】都会叠加在玩家身上一个计数器上。
    • limitCount:叠加上限。
      • 需要注意一点,叠加溢出的效果触发是在叠加计数【大于】limitCount时触发。举个例子,如果某个buff叠加3层后触发爆炸伤害,那limitCount应该是2。
    • DurationRefreshPolicy:持续时间刷新策略。GE叠加成功后,GE的持续时间的刷新策略。
      • DurationRefreshPolicy 作用
        NeverRefresh 从不刷新持续时间。即叠加的BUFF持续时间从第一层生效后计时就不再受影响。
        RefreshOnSuccessfulApplication 每次Effect叠加apply成功后刷新Effect的持续时间。
    • PeriodResetPolicy:周期重置策略。GE叠加成功后,GE的周期(Period)的刷新策略。
      • PeriodResetPolicy 作用
        NeverReset 从不重置周期。
        ResetOnSuccessfulApplication 每次apply成功后重置Effect的周期计时。
    • ExpirationPolicy:过期策略(持续时间结束时逻辑处理)。GE叠加成功后,GE的过期时间(Expiration)的刷新策略。
      • ExpirationPolicy 作用
        ClearEntireStack 持续时间结束时,清楚所有层数
        RemoveSingleStackAndRefreshDuration 持续时间结束时减少一层,然后重新经历一个Duration,一直持续到层数减为0
        RefreshDuration 持续时间结束时,再次刷新Duration,这相当于无限Duration。
    • denyOverflowApplication:布尔类型。是否允许溢出的GE叠加生效。
      • 对应于DurationRefreshPolicy = RefreshOnSuccessfulApplication时,如果为true则多余的Apply不会刷新Duration
    • clearStackOnOverflow: 布尔类型。是否溢出时清空所有层数,移除GE。
      • 当DenyOverflowApplication为True是才有效,当Overflow时是否直接删除所有层数,移除GE。
    • overflowEffects:GameplayEffect的数组,溢出时施加的游戏效果。当Stack计数溢出时,对生效单位执行这些GE。

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

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

2.8 Ability

Ability是EX-GAS的核心类之一,它是游戏中的所有能力基础。

同时Ability也是程序开发人员最常接触的类,Ability的完整逻辑都是由程序开发人员实现的。

在EX-GAS内,Ability是游戏中可以触发的一切行为和技能。多个Ability可以在同一时刻激活, 例如移动和持盾防御。 Ability作为EX-GAS的核心类之一,他起到了Do(做)的功能。

Ability的业务逻辑取决于游戏类型和玩法。所以不存在一个通用的Ability模板,当然可以针对游戏类型制作一些通用的ability。 Ability的逻辑并非自由,如果胡乱的实现Ability逻辑,可能会导致游戏逻辑混乱,所以需要遵循一些规则。

Ability的具体实现需要策划和程序配合。 这并不是废话,而是在EX-GAS的Ability制作流程中,确确实实的把策划和程序的工作分开了:

  • 策划的工作:配置AbilityAsset
  • 程序的工作:编写Ability(AbilityAsset,Ability,AbilitySpec)类
  • 功能
    AbilityAsset Ability的配置文件,同一类的Ability可以通过配置不同的AbilityAsset参数,实现复数Ability,比如跳跃段数不同,可以实现普通跳,二段跳。
    Ability Ability的Runtime数据类,通常数据还是依赖AbilityAsset。同时也允许运行时不依赖AbilityAsset生成Ability
    AbilitySpec Ability的运行实例,Ability的游戏内的表现逻辑,就在该类中实现。

建议 AbilitySpec和Ability在同一个脚本中编辑,因为二者本身就是成对出现。AbilityAsset单独一个脚本,因为它是Scriptable Object,应该遵从脚本名和类一致的原则。

Ability运作逻辑的组成可以拆成两部分:

  • GAS系统内的运作逻辑:所有Ability通用的数据字段,被保存在AbstractAbility这个抽象基类中。所有的Ability都是继承自AbstractAbility。
  • 具体游戏内的表现逻辑:每个Ability都有自己的表现逻辑,这部分逻辑是由程序开发人员自行实现的。

接下来结合Ability的配置界面来解释Ability的数据和运作逻辑。

2.8.a Ability编辑界面

QQ20240313162642.png

图中A的部分就是GAS系统内的运作逻辑的参数,B的部分就是具体游戏内的表现逻辑的参数。

注意到最上方显示了Ability Class,这是Ability的类名,与之成对的AbilitySpec决定了Ability游戏内的表现逻辑。

  • GAS系统内的运作逻辑参数
    • U-Name: Ability的名称,唯一标识符,禁止重复或空。U-Name会用于AbilityLib的Ability生成和查找。
    • Description: Ability的描述,纯粹用于显示,不会影响游戏逻辑。方便编辑者区分Ability。
    • 消耗Cost:Ability的消耗GameplayEffect
    • 冷却CD:Ability的冷却GameplayEffect
    • CD时间:冷却时间,它会覆盖冷却GameplayEffect的持续时间。
    • Tags: Ability的标签,与GameplayEffect的Tag些许类似。具体Tag功能如下
Tag 功能
Asset Tag 描述性质的标签,用来描述Ability的特性表现,比如伤害、治疗、控制等
CancelAbility With Tags Ability激活时,Ability持有者当前持有的所有Ability中,拥有**【任意】**这些标签的Ability会被取消
BlockAbility With Tags Ability激活时,Ability持有者当前持有的所有Ability中,拥有**【任意】**这些标签的Ability会被阻塞激活
Activation Owned Tags Ability激活时,持有者会获得这些标签,Ability被失活时,这些标签也会被移除
Activation Required Tags Ability只有在其拥有者拥有**【所有】**这些标签时才可激活
Activation Blocked Tags Ability在其拥有者拥有**【任意】**这些标签时不能被激活
  • 具体游戏内的表现逻辑参数

    • 这部分的参数面板是由程序开发人员自行实现的,这部分参数的含义和作用不固定。
    • 建议使用Odin的特性编辑,一是为了规范,二是美观。
    • 不同类的Ability,这部分的面板显示和含义不同。如下图: QQ20240313165819.png
  • 【!!注意!!】

    • 当创建或修改AbilityAsset的U-Name后,一定要去Ability汇总界面点击【生成AbilityLib】按钮。 AbilityLib.gen.cs脚本中会包含所有的Ability信息。 游戏运行时生成Ability依赖于AbilityLib,如果没有保持同步,会导致游戏运行时找不到Ability。
    • Ability汇总界面入口:在菜单栏EX-GAS -> Asset Aggregator -> 左侧菜单列点击 C-Ability QQ20240313175247.png

2.8.b TimelineAbility 通用性Ability(W.I.P TimelineAbility还在完善中)

在实际的开发过程中,我发现,许多的Ability都有顺序和时限两个特点。 每次都新写一个Ability类来实现某个指定技能让我十分烦躁,于是我制作了TimelineAbility,一个极具通用性的顺序,时限Ability。 TimelineAbilityAsset.png

这是TimelineAbilityAsset的面板。

TimelineAbilityAsset的大多数表现逻辑参数在AbilityAsset面板都是隐藏的(HideInInspector)。 转而都是在TimelineAbilityEditor面板中可视化编辑。 唯一在AbilityAsset面板中可见的参数是【手动结束能力】的bool值选项。这个选项决定Ability是手动结束还是播放完成后自动结束。

通过点击【查看/编辑能力时间轴】按钮,可以打开TimelineAbilityEditor面板。 TimelineAbilityEditor.png

这是TimelineAbilityEditor的面板。

接下来详细介绍TimelineAbilityEditor的面板参数含义及操作逻辑。

  • 顶部菜单栏 QQ20240315133711.png
    • Ability配置 : 当前编辑的TimelineAbilityAsset
    • 查看能力基本信息:点击按钮后,右侧的子Inspector会显示TimelineAbilityAsset的面板信息,和Asset Aggregator中的AbilityAsset面板一致。
    • 预览实例:场景内的GameObject. 选取后,TimelineAbility会以该GameObject为参照物,预览TimelineAbility的表现逻辑。 用到的常见预览有Animation,特效,物体挂载,伤害碰撞盒等等。
    • 预览场景:点击该按钮后会,进入空场景。目前还没有自定义LookDev场景,所以只是一个临时创建空场景。
    • 返回旧场景:点击该按钮后会,返回到原来的场景。
    • 显示子Inspector:点击该按钮后会,刷新显示右侧的子Inspector的布局。
  • 时间轴编辑部分
    • 左侧播放栏 QQ20240315143604.png

      • 【<】:上一帧,只有当预览实例不为空时,才会生效。
      • 【>】:下一帧,只有当预览实例不为空时,才会生效。
      • 【▶】:播放/暂停 ,只有当预览实例不为空时,才会生效。
      • 左侧帧数:当前预览帧数,只有当预览实例不为空时,才会生效。
      • 右侧帧数:Ability执行总帧数
    • 左侧轨道菜单栏

      • QQ20240315144012.png
      • TimelineAbility基础轨道有6种。【添加轨道只需点击右侧的‘+’,删除轨道只需右键对应轨道选择Delete Track即可】
        1. Instant Cue 【即时Cue轨道】
          • 轨道Item类型:Mark QQ20240315151141.png
          • 一个Mark可以挂多个Cue。理论上,一个TimelineAbility只需要一条Instant Cue轨道。 QQ20240315152528.png
          • 扩展:详见上文中提到的Instant Cue自定义实现
        2. Release Effect【GameplayEffect释放轨道】
          • 轨道Item类型:Mark

          • 一个Mark持有一个TargetCatcher和数个GameplayEffectAsset QQ20240315153247.png

            • TargetCatcher:GameplayEffect释放需要对象,而TargetCatcher的作用就是找到这些对象。 TargetCatcher固有初始化会获取Owner(ASC),核心是方法CatchTargets()。 基类如下:
              public abstract class TargetCatcherBase
              {
                 public AbilitySystemComponent Owner;
                 public TargetCatcherBase()
                 {
                 }
                 public virtual void Init(AbilitySystemComponent owner)
                 {
                     Owner = owner;
                 }
                 // mainTarget为TimelineAbility的指向性目标单位,为可选参数。具体在API中会介绍。
                 public abstract List<AbilitySystemComponent> CatchTargets(AbilitySystemComponent mainTarget);
              }
            

            我提供了几个基础TargetCatcher(后续会陆续添加常用的Catcher)

            Catcher名 作用
            CatchSelf 捕捉自己
            CatchTarget 捕捉指向性目标
            CatchAreaBox2D 捕捉2d矩形内的目标(适用2D游戏)
            CatchAreaCircle2D 捕捉2d圆形内的目标(适用2D游戏)
            • TargetCatcher的UI面板绘制: 自定义TargetCatcher的UI面板需要继承自TargetCatcherInspector T为TargetCatcher类 (必须直接继承,不可以多级继承,因为Inspector的Type查找规则依赖第一泛型类做匹配)
          • Release Effect的执行逻辑:先调用TargetCatcher的CatchTargets()方法,然后对捕获的目标单位施加所有指定GameplayEffect。

        3. Instant Task 【即时Task轨道】
          • 轨道Item类型:Mark
          • 一个Mark可以挂载复数的Instant Task。 关于Ability Task的详细介绍见下文QQ20240315170234.png
          • Task是自定义事件,可以是任何游戏逻辑,纯粹由开发者决定。
          • Instant Task的面板绘制:自定义Task的UI面板需要继承自InstantTaskInspector T为InstantAbilityTask类 (必须直接继承,不可以多级继承,因为Inspector的Type查找规则依赖第一泛型类做匹配)
        4. Durational Cue【持续GameplayCue轨道】
          • 轨道Item类型:Clip QQ20240315164448.png
          • 每段Clip只含一个Duration Cue QQ20240315164632.png
          • 注意!! TimelineAbility下的持续性Cue, 只会执行OnAdd(Cue播放的第一帧),OnRemove(Cue播放的最后一帧),OnTick,和GameplayEffect相关的方法不会被执行
        5. Buff【Buff轨道】
          • 轨道Item类型:Clip
          • 每段Clip只含一个Buff(GameplayEffect),且Buff的作用对象只会是Ability的持有者自己。 QQ20240315165106.png
          • 【注意!!】请确保设置的GameplayEffect类型为Durational或Infinite。 非持续类型的GameplayEffect不会生效。且GameplayEffect执行时会设置为Infinite执行策略, 生命周期由Clip长度(Duration)决定。
        6. Ongoing Task【持续Task轨道】
          • 轨道Item类型:Clip
          • 每段Clip只含一个Ongoing Task。 关于Ability Task的详细介绍见下文QQ20240315170648.png
          • Ongoing Task的面板绘制:自定义Task的UI面板需要继承自OngoingTaskInspector T为OngoingAbilityTask类 (必须直接继承,不可以多级继承,因为Inspector的Type查找规则依赖第一泛型类做匹配)
    • 右侧Inspector

      • 所有Track,TrackItem参数在点击后都会显示在Inspector上

TimelineAbility的执行逻辑很直观,就是沿着时间轴从左往右执行,每个轨道的Item都会在对应的时间点执行。 所有事件的执行,都遵照时间轴的顺序,以及Mark内参数排列顺序。如果存在前后逻辑关系,那么配置的时候请务必注意顺序。 最大帧数决定了Ability的执行时间,如果【手动结束能力】设置为false,那么在播放完TimelineAbility后,会自动调用TryEndAbility(), 反之,需要开发人员在代码中决定调用TryEndAbility()的时机。

TimelineAbility的配置可能还满足不了一些设计时,程序开发人员可以对TimelineAbility进行继承,拓展功能需求。

特别是在TargetCatcher,AbilityTask的自定义上,还是很可能遇到这个问题。 因为TargetCatcher和AbilityTask的持续化存储是以JsonData的格式,ScriptableObject类型参数的Json存储是存在GUID不匹配问题的。 所以,TargetCatcher和AbilityTask的参数中,不建议出现ScriptableObject类型的参数。

2.8.c Granted Ability From GameplayEffect 来自游戏效果授予的能力

能力不仅仅可以由AbilitySystemComponent直接授予,还可以通过GE来授予,甚至是GE来全权控制。

我们为了更通俗的去理解BUFF的概念,就必须允许GE可以实现自由的逻辑自定义。 但是GAS本身的GE仅仅是遵循体系内固定逻辑,不存在开发者自定义GE逻辑。 GAS为GE提供了Granted Ability的解决方案。

为了更好理解这种情况,举个例子:

在一个RPG游戏中,有一个名为“亡灵收割”的BUFF。 “亡灵收割”效果为:BUFF持有者,在x米范围内,每当有单位死亡便获得y点生命值。

一般的做法可能是把“亡灵收割”视作一个被动技能(Ability),然后根据设计需求动态的添加/移除/激活/失活“亡灵收割”效果。 可能有些设计者为了使之更符合BUFF的逻辑,会通过GE的Add/Remove/Active/Deactive回调,来关联“亡灵收割”添加/移除/激活/失活。

上述例子的这个做法当然是没问题的,而且十分合理。 而在EX-GAS中,我为了减少Ability管理的事件注册这个繁琐步骤。我对GE的逻辑进行了优化,兼并了这一设计方案。 做法就是在GE中,添加了GrantedAbility变量。 GrantedAbility有5个参数:

  • Ability:授予的能力(数据)
  • AbilityLevel:授予的能力等级
  • ActivationPolicy: 授予的能力的激活策略,类型如下:
    • 激活策略 作用
      None 无激活逻辑, 需要用户自己调用ASC能力激活接口
      WhenAdded 能力添加时激活(GE添加时激活)
      SyncWithEffect 同步GE,GE激活时激活
  • DeactivationPolicy: 授予的能力的取消激活策略,类型如下:
    • 取消激活策略 作用
      None 无相关取消激活逻辑, 需要用户调用ASC能力取消激活接口
      SyncWithEffect 同步GE,GE失活时取消激活
  • RemovePolicy: 授予的能力的移除策略,类型如下:
    • 移除策略 作用
      None 不移除
      SyncWithEffect 同步GE,GE移除时移除
      WhenEnd 能力结束时自己移除
      WhenCancel 能力取消时自己移除
      WhenCancelOrEnd 能力结束或取消时自己移除

到这里Granted Ability的逻辑就清晰了。我们提前将Ability的生命周期通过参数,来确定哪些阶段交给GE来管理。

有一点需要注意,Granted Ability的激活不会传任何参数,请保证Ability执行逻辑中依赖的参数,都可以通过Owner(ASC)直接或间接获取。

Granted Ability只是EX-GAS给出的一个现成设计方案,依然可以通过各个事件监听/回调,来实现同样的效果。


2.9 AbilitySystemComponent

AbilitySystemComponent是EX-GAS的核心之一,它是GAS的基本运行单位。

ASC(之后都使用缩写指代AbilitySystemComponent),持有Tag,Ability,AttributeSet,GameplayEffect等数据。 其主要职责如下:

  • 管理能力(Abilities): ASC 负责管理角色的所有能力。它允许角色获得、激活、取消和执行各种不同类型的能力,如攻击、防御、技能等。
  • 处理效果(GameplayEffects): ASC 负责处理与能力相关的效果,包括伤害、治疗、状态效果等。它能够跟踪和应用这些效果,并在需要时触发相应的回调或事件。
  • 处理标签(Tags): ASC 负责管理角色身上的标签。标签用于标识角色的状态、属性或其他特征,以便在能力和效果中进行条件检查和过滤。
  • 处理属性(Attributes): ASC 负责管理角色的属性。属性通常表示角色的状态,如生命值、能量值等。ASC 能够增减、修改和监听这些属性的变化。

整个GAS的运作都是围绕着ASC的,所有的Tag,GameplayEffect的作用对象最后都是ASC。而Ability也必须依赖ASC来执行。 为了直观的理解ASC,接下来参考GAS Watcher的监视器界面: QQ20240313180923.png

  • ID Mark:这部分可以忽略,只是Unity运行时的Gameobject的标识符相关
  • Ability: 图中显示了这个单位有6个Ability。Ability表示了单位的持有能力,但不代表所有的单位能力都要由Ability来做。 虽然我把Move作为Ability了,但是在FPS,MOBA等类型的游戏中,像Move移动这种强Input关联的行为是不建议用Ability来做的。
  • Attribute:单位持有的属性。 ASC的属性是以AttributeSet为集合,通常ASC的属性集,只加不减。 因为删除属性集存在很大的隐患,在GAS体系运作时,属性相当于串联GAS运行的线,线上挂着许多GameplayEffect,贸然删除属性集可能会导致线断裂,GameplayEffect运行逻辑失效甚至崩溃。
  • GameplayEffects:单位当前持有的GameplayEffect。这里可以直接将GameplayEffect理解为Buff。 GAS体系内,ASC之间是通过GameplayEffect来交互。 比如A单位对B单位施法,那么A单位会通过施法(Ability)对B施加一个效果(GameplayEffect),效果如果是持续性的就会被留在B身上(Buff)。 当然,GAS体系外时,可以通过接口对ASC单位添加/移除GameplayEffect。通常这种情况下,Effect的来源和目标都是指定的那个ASC。
  • Tags:单位当前持有的Tag。标签的价值只有在被挂载到ASC时才会体现出来。所有Tag的比较逻辑都直接或间接依赖于ASC。
    • Fixed Tag:固有标签,这些标签是ASC固有的。GAS体系内是不会对FixedTag做任何增减的。 只有GAS体系外的才会对FixedTag有影响。FixedTag通常是在ASC初始化的时候设置好,之后就尽可能不动。
    • Dynamic Tag:动态标签,这些标签是ASC动态增减的。GAS体系只会对DynamicTag做增减,且只有GAS体系可以管理。 GAS体系外只能通过GameplayEffect的一些指定接口,间接对DynamicTag进行操作。

ASC是GAS中最复杂,且操作空间最多的组件。对ASC的良好管理和操作就是程序开发人员的重任了。 GAS本身是被动的,而让推动和改变GAS的是ASC。换言之,Runtime下开发者其实是在操作ASC,而不是GAS。 增删管理ASC,调用ASC的Ability执行,以及ASC的体系外Tag,Effect管理才是Runtime下开发者的主要工作。 这之外的GAS配置和拓展,应该由策划承担大部分工作。(但实际上对于中小型团队,程序开发人员还是在做GAS配置的维护工作。)

2.9.a AbilitySystemComponent Preset

AbilitySystemComponent Preset是ASC的预设,用于方便初始化ASC的数据。 QQ20240315172608.png ASC预设是为了可视化角色(单位)的参数。

  • 基本信息:ASC的基本信息,仅用于显示,方便配置人员阅读,Runtime不会用到这些参数。
  • 属性集:上文提到过,ASC的属性集设计建议只有一个属性集。不建议多个。
  • 固有Tag:ASC的基础Tag,通常会把描述性的Tag作为固有Tag, 比如种族(Race.Human,Race.Monster ),职业(Job.Wizard,Job.Archer),阵营(Camp.Good,Camp.Evil)等等。 当然Tag本身是不做任何限制的,但从Gameplay设计的角度上,状态性质的Tag不建议作为固有Tag。就算设计一个绝对无敌的 角色,那也应该是把无敌的Tag放在一个永久GameplayEffect上,然后挂到ASC上。而不是把无敌Tag直接当作固有Tag。
  • 固有能力:Abilities,单位的基础能力。通常会把单位的基础能力放在这里,比如攻击,防御,跳跃等等。

如何使用ASC预设?

1.AbilitySystemComponent组件自带了序列化的ASC预设字段,可以通过预制体添加,也可以实例化添加。 2.依赖ASC预设的初始化,通过AbilitySystemComponentExtension中的静态扩展方法InitWithPreset即可。

InitWithPreset的参数:

  • AbilitySystemComponent asc:初始化的ASC,
  • int level:初始等级
  • AbilitySystemComponentPreset preset:初始化用的ASC预设

示例: asc.InitWithPreset(1,ascPreset); // 如果预制体已经设置了参数,那么可以不传ascPreset。


3.API && Source Code Documentation

本章节会在介绍API和源码的同时,从代码的角度来理解GAS的运作逻辑。 GAS_IMG_Intro.png

该图简单的解释了GAS的运作逻辑。GAS其实简单的只干一件事:ASC使用Ability对指定的(可以包括自己)ASC释放GameplayEffect。

GAS的推进和运行,就是在不断的重复这件事。 体系外的脚本不断的拨动ASC的Ability,而GAS内部会对Ability的运行结果自行消化。

3.1 Core

3.1.1 GameplayAbilitySystem

GameplayAbilitySystem作为核心类,他的作用有2个:管理ASC,控制GAS的运行与否。

  • static GameplayAbilitySystem GAS
    • GAS的单例,所有的GAS操作都是通过GAS单例来进行的。
  • List<AbilitySystemComponent> AbilitySystemComponents { get; }
    • GAS当前运行的所有AbilitySystemComponent的集合。
  • void Register(AbilitySystemComponent abilitySystemComponent)
    • 注册ASC到GAS中。
  • void UnRegister(AbilitySystemComponent abilitySystemComponent)
    • 从GAS中注销ASC。
  • bool IsPaused
    • GAS是否暂停运行。
  • void Pause()
    • 暂停GAS运行。
  • void Unpause()
    • 恢复GAS运行。

3.1.2 GASTimer

GASTimer是GAS的计时器,它是GAS的时间基准。

  • static long Timestamp()
    • GAS当前时间戳(毫秒)
  • static long TimestampSeconds()
    • GAS当前时间戳(秒)
  • static int CurrentFrameCount
    • GAS当前运行帧数
  • static long StartTimestamp
    • GAS启动时间戳
  • static void InitStartTimestamp()
    • GAS初始化启动时间戳
  • static void Pause()
    • 暂停GASTimer计时
  • static void Unpause()
    • 恢复GASTimer计时
  • static int FrameRate
    • GAS帧率

3.1.3 GasHost

GasHost是GAS的宿主,它是GAS的运行机器和环境,GasHost没有API可以从外部干涉。


3.2 AbilitySystemComponent

3.2.1 AbilitySystemComponent

AbilitySystemComponent是GAS的基本运行单位,它是GAS的核心类。 ASC的public方法和属性就是外部干涉GAS的唯一手段。

  • AbilitySystemComponentPreset Preset
    • ASC的预设。外部读取用,修改preset需要通过SetPreset方法
  • void SetPreset(AbilitySystemComponentPreset preset)
    • 修改ASC的预设。
  • int Level { get; protected set; }
    • ASC的等级
  • GameplayEffectContainer GameplayEffectContainer { get; private set; }
    • ASC当前所有GameplayEffect的容器,可以通过GameplayEffectContainer对GameplayEffect进行一定的外部干涉。
  • GameplayTagAggregator GameplayTagAggregator { get; private set;}
    • ASC的GameplayTag聚合器,单位的Tag全部都由聚合器管理,外部可以通过聚合器对Tag进行一定的外部干涉。
  • AbilityContainer AbilityContainer { get; private set;}
    • ASC的Ability容器,可以通过AbilityContainer对Ability进行一定的外部干涉。
  • AttributeSetContainer AttributeSetContainer { get; private set;}
    • ASC的AttributeSet容器,可以通过AttributeSetContainer对AttributeSet进行一定的外部干涉。
  • void Init(GameplayTag[] baseTags, Type[] attrSetTypes,AbilityAsset[] baseAbilities,int level)
    • 初始化ASC
    • baseTags:ASC的基础Tag
    • attrSetTypes:ASC的初始化AttributeSet类型
    • baseAbilities:ASC的初始化Ability
    • level:ASC的初始化等级
  • void SetLevel(int level)
    • 设置ASC的等级
  • bool HasTag(GameplayTag gameplayTag)
    • 判断ASC是否持有指定Tag
    • gameplayTag:指定Tag
    • 返回值:是否持有
  • bool HasAllTags(GameplayTagSet tags)
    • 判断ASC是否持有指定Tag集合中的所有Tag
    • tags:指定Tag集合
    • 返回值:是否持有
  • bool HasAnyTags(GameplayTagSet tags)
    • 判断ASC是否持有指定Tag集合中的任意一个Tag
    • tags:指定Tag集合
    • 返回值:是否持有
  • void AddFixedTags(GameplayTagSet tags)
    • 添加固有Tag
    • tags:添加的Tag集合
  • void RemoveFixedTags(GameplayTagSet tags)
    • 移除固有Tag
    • tags:移除的Tag集合
  • void AddFixedTag(GameplayTag tag)
    • 添加固有Tag
    • tag:添加的Tag
  • void RemoveFixedTag(GameplayTag tag)
  • 移除固有Tag
  • tag:移除的Tag
  • void RemoveGameplayEffect(GameplayEffectSpec spec)
    • 移除指定的GameplayEffect
    • spec:指定的GameplayEffect的规格类实例
  • GameplayEffectSpec ApplyGameplayEffectTo(GameplayEffect gameplayEffect, AbilitySystemComponent target)
    • 对指定的ASC施加指定的GameplayEffect
    • gameplayEffect:指定的GameplayEffect
    • target:目标ASC
    • 返回值:施加的GameplayEffect的规格类实例
  • GameplayEffectSpec ApplyGameplayEffectToSelf(GameplayEffect gameplayEffect)
    • 对自己施加指定的GameplayEffect
    • gameplayEffect:指定的GameplayEffect
    • 返回值:施加的GameplayEffect的规格类实例
  • void GrantAbility(AbstractAbility ability)
    • 获得指定的Ability
    • ability:指定的Ability
  • void RemoveAbility(string abilityName)
    • 移除指定的Ability
    • abilityName:指定的Ability的U-Name
  • float? GetAttributeCurrentValue(string setName, string attributeShortName)
    • 获取指定Attribute的当前值
    • setName:AttributeSet的名字
    • attributeShortName:Attribute的短名
    • 返回值:Attribute的当前值
  • float? GetAttributeBaseValue(string setName, string attributeShortName)
    • 获取指定Attribute的基础值
    • setName:AttributeSet的名字
    • attributeShortName:Attribute的短名
    • 返回值:Attribute的基础值
  • Dictionary<string, float> DataSnapshot()
    • 获取ASC的数据快照
    • 返回值:ASC的数据快照
  • bool TryActivateAbility(string abilityName, params object[] args)
    • 尝试激活指定的Ability
    • abilityName:指定的Ability的U-Name
    • args:激活Ability的参数
    • 返回值:是否激活成功
  • void TryEndAbility(string abilityName)
    • 尝试结束指定的Ability
    • abilityName:指定的Ability的U-Name
  • void TryCancelAbility(string abilityName)
    • 尝试取消指定的Ability
    • abilityName:指定的Ability的U-Name
  • void ApplyModFromInstantGameplayEffect(GameplayEffectSpec spec)
    • 从Instant GameplayEffect中应用Mod
    • spec:Instant GameplayEffect的规格类实例
  • CooldownTimer CheckCooldownFromTags(GameplayTagSet tags)
    • 通过Tag检查冷却时间
    • tags:指定的Tag集合
    • 返回值:冷却计时器
  • T AttrSet<T>() where T : AttributeSet
    • 获取指定类的AttributeSet
    • 返回值:指定类的AttributeSet
  • void ClearGameplayEffect()
    • 清空ASC的所有GameplayEffect

3.2.2 AbilitySystemComponentPreset

AbilitySystemComponentPreset是ASC的预设,用于方便初始化ASC的数据。

  • string[] AttributeSets
    • ASC的初始化AttributeSet类型
  • GameplayTag[] BaseTags
    • ASC的基础Tag
  • AbilityAsset[] BaseAbilities
    • ASC的初始化Ability

3.2.3 AbilitySystemComponentExtension

AbilitySystemComponentExtension是ASC的扩展方法类,用于方便ASC的初始化和操作。 AbilitySystemComponentExtension不是EX-GAS框架内脚本的,需要EX-GAS框架基础配置完成后,通过生成脚本生成。

  • static Type[] PresetAttributeSetTypes(this AbilitySystemComponent asc)
    • 获取ASC的预设AttributeSet类型
    • 返回值:ASC的预设AttributeSet类型
  • static GameplayTag[] PresetBaseTags(this AbilitySystemComponent asc)
    • 获取ASC的预设基础Tag
    • 返回值:ASC的预设基础Tag
  • static void InitWithPreset(this AbilitySystemComponent asc,int level, AbilitySystemComponentPreset preset = null)
    • 通过预设初始化ASC
    • level:ASC的初始化等级
    • preset:ASC的预设

3.3 GameplayTag

3.3.1 GameplayTag

GameplayTag是GAS的标签类,它是GAS的核心类。Tag的设计结构虽然简单,但是在实际应用中十分高效有用。

  • int HashCode => _hashCode;
    • Tag的HashCode
  • string[] AncestorNames => _ancestorNames;
    • Tag的父级名
  • int[] AncestorHashCodes => _ancestorHashCodes;
    • Tag的父级HashCode集合
  • bool Root => _ancestorHashCodes.Length == 0;
    • Tag是否是根Tag
  • bool IsDescendantOf(GameplayTag other)
    • Tag是否是指定Tag的子Tag
    • other:指定Tag
    • 返回值:是否是子Tag
  • bool HasTag(GameplayTag tag)
    • Tag是否持有指定Tag,比如‘Buff.Burning’ 持有 ‘Buff’
    • tag:指定Tag
    • 返回值:是否持有

3.3.2 GameplayTagSet

GameplayTagSet是Tag集合类之一。GameplayTagSet适用于稳定不会改变的Tag集合。通常数据类的Tag集合都用GameplayTagSet。

  • readonly GameplayTag[] Tags
    • Tag数据
  • bool Empty => Tags.Length == 0;
    • Tag集合是否为空
  • bool HasTag(GameplayTag tag)
    • TagSet是否持有指定Tag
    • tag:指定Tag
    • 返回值:是否持有
  • bool HasAllTags(GameplayTagSet other) / bool HasAllTags(params GameplayTag[] tags)
    • TagSet是否持有指定Tag集合中的所有Tag
    • other:指定Tag集合
    • 返回值:是否持有
  • bool HasAnyTags(GameplayTagSet other) / bool HasAnyTags(params GameplayTag[] tags)
    • TagSet是否持有指定Tag集合中的任意一个Tag
    • other:指定Tag集合
    • 返回值:是否持有
  • bool HasNoneTags(GameplayTagSet other) / bool HasNoneTags(params GameplayTag[] tags)
    • TagSet是否不持有指定Tag集合中的所有Tag
    • other:指定Tag集合
    • 返回值:是否不持有

3.3.3 GameplayTagContainer

GameplayTagContainer是Tag集合类之一。GameplayTagContainer适用于经常改变的Tag集合。

  • List<GameplayTag> Tags { get; }
    • Tag数据
  • void AddTag(GameplayTag tag)
    • 添加Tag
    • tag:指定Tag
  • void AddTag(GameplayTagSet tagSet)
    • 添加Tag集合
    • tagSet:要添加的Tag集合
  • void RemoveTag(GameplayTag tag)
    • 移除Tag
    • tag:指定Tag
  • void RemoveTag(GameplayTagSet tagSet)
    • 移除Tag集合
    • tagSet:要移除的Tag集合
  • bool HasTag(GameplayTag tag)
    • TagContainer是否持有指定Tag
    • tag:指定Tag
  • bool HasAllTags(GameplayTagSet other) / bool HasAllTags(params GameplayTag[] tags)
    • TagContainer是否持有指定Tag集合中的所有Tag
    • other:指定Tag集合
    • 返回值:是否持有
  • bool HasAnyTags(GameplayTagSet other) / bool HasAnyTags(params GameplayTag[] tags)
    • TagContainer是否持有指定Tag集合中的任意一个Tag
    • other:指定Tag集合
    • 返回值:是否持有
  • bool HasNoneTags(GameplayTagSet other) / bool HasNoneTags(params GameplayTag[] tags)
    • TagContainer是否不持有指定Tag集合中的所有Tag
    • other:指定Tag集合
    • 返回值:是否不持有

3.3.4 GameplayTagAggregator

GameplayTagAggregator是专门针对ASC的Tag管理类,会针对固有Tag和动态Tag做不同的处理。

  • void Init(GameplayTag[] tags)
    • 初始化
    • tags:初始化的固有Tag
  • void AddFixedTag(GameplayTag tag)
    • 添加固有Tag
    • tag:添加的Tag
  • void AddFixedTag(GameplayTagSet tagSet)
    • 添加固有Tag集合
    • tagSet:添加的Tag集合
  • void RemoveFixedTag(GameplayTag tag)
    • 移除固有Tag
    • tag:移除的Tag
  • void RemoveFixedTag(GameplayTagSet tagSet)
    • 移除固有Tag集合
    • tagSet:移除的Tag集合
  • void ApplyGameplayEffectDynamicTag(GameplayEffectSpec source)
    • 从GameplayEffect中应用动态Tag(Granted Tags)
    • source:GameplayEffect的规格类实例
  • void ApplyGameplayAbilityDynamicTag(AbilitySpec source)
    • 从Ability中应用动态Tag(Activation Owned Tags)
    • source:Ability的规格类实例
  • RestoreGameplayEffectDynamicTags(GameplayEffectSpec effectSpec)
    • 从GameplayEffect中恢复动态Tag(Granted Tags)
    • effectSpec:GameplayEffect的规格类实例
  • RestoreGameplayAbilityDynamicTags(AbilitySpec abilitySpec)
    • 从Ability中恢复动态Tag(Activation Owned Tags)
    • abilitySpec:Ability的规格类实例
  • bool HasTag(GameplayTag tag)
    • TagAggregator是否持有指定Tag
    • tag:指定Tag
    • 返回值:是否持有
  • bool HasAllTags(GameplayTagSet other) / bool HasAllTags(params GameplayTag[] tags)
    • TagAggregator是否持有指定Tag集合中的所有Tag
    • other:指定Tag集合
    • 返回值:是否持有
  • bool HasAnyTags(GameplayTagSet other) / bool HasAnyTags(params GameplayTag[] tags)
    • TagAggregator是否持有指定Tag集合中的任意一个Tag
    • other:指定Tag集合
    • 返回值:是否持有
  • bool HasNoneTags(GameplayTagSet other) / bool HasNoneTags(params GameplayTag[] tags)
    • TagAggregator是否不持有指定Tag集合中的所有Tag
    • other:指定Tag集合
    • 返回值:是否不持有

3.3.5 GTagLib(Script-Generated Code)

GTagLib是GAS的标签库,它是GAS的标签管理类。 GTagLib不是EX-GAS框架内脚本的,需要EX-GAS框架Tag配置改动后,通过生成脚本生成。

  • public static GameplayTag XXX { get;} = new GameplayTag("XXX");
  • public static GameplayTag XXX_YYY { get;} = new GameplayTag("XXX.YYY");
    • GTagLib会把所有的Tag都生成为静态字段,方便外部调用。格式如上所示。
    • A.B.C的Tag会生成为A_B_C的静态字段。
  • public static Dictionary<string, GameplayTag> TagMap = new Dictionary<string, GameplayTag> { ["A"] = A, ["A.B"] = A_B, ["A.C"] = A_C, };
    • GTagLib还包含了一个TagMap,方便外部通过Tag的字符串名来获取Tag。

3.4 Attribute

3.4.1 AttributeValue

AttributeValue是一个数据结构体。是实际存储Attribute的值的单位。

  • float BaseValue => _baseValue;
    • Attribute的基础值,是属性,只读。修改baseValue需要通过AttributeBase的SetBaseValue方法
  • float CurrentValue => _currentValue;
    • Attribute的当前值,是属性,只读。修改currentValue需要通过AttributeBase的SetCurrentValue方法
  • void SetBaseValue(float value)
    • 设置Attribute的基础值
    • value:指定的值
  • void SetCurrentValue(float value)
    • 设置Attribute的当前值
    • value:指定的值

3.4.2 AttributeBase

AttributeBase是GAS的属性基类,它是GAS的核心类之一。 负责管理AttributeValue的值变化,已经Attribute相关回调处理。

  • readonly string Name
    • Attribute的名字(完整)
  • readonly string ShortName
    • Attribute的短名
  • readonly string SetName
    • Attribute所属的AttributeSet的名字
  • AttributeValue Value => _value;
    • Attribute的值类,数据类
  • float BaseValue => _value.BaseValue;
    • Attribute的基础值
  • float CurrentValue => _value.CurrentValue;
    • Attribute的当前值
  • void SetCurrentValue(float value)
    • 设置Attribute的当前值,会触发_onPreCurrentValueChange和_onPostCurrentValueChange回调
    • value:指定的值
  • void SetBaseValue(float value)
    • 设置Attribute的基础值,会触发_onPreBaseValueChange和_onPostBaseValueChange回调
    • value:指定的值
  • void SetCurrentValueWithoutEvent(float value)
    • 设置Attribute的当前值,但不会触发_onPreCurrentValueChange和_onPostCurrentValueChange回调
    • value:指定的值
  • void SetBaseValueWithoutEvent(float value)
    • 设置Attribute的基础值,但不会触发_onPreBaseValueChange和_onPostBaseValueChange回调
    • value:指定的值
  • void RegisterPreBaseValueChange(Func<AttributeBase, float,float> func)
    • 注册Attribute的基础值变化前回调
    • func:回调函数
      • AttributeBase:AttributeBase实例
      • float:变化前的值
      • float:准备变化的值
      • 返回值:回调处理完的变化值
  • void RegisterPostBaseValueChange(Action<AttributeBase, float, float> action)
    • 注册Attribute的基础值变化后回调
    • action:回调函数
      • AttributeBase:AttributeBase实例
      • float:变化前的值
      • float:变化后的实际值
  • void RegisterPreCurrentValueChange(Func<AttributeBase, float, float> func)
    • 注册Attribute的当前值变化前回调
    • func:回调函数
      • AttributeBase:AttributeBase实例
      • float:变化前的值
      • float:准备变化的值
      • 返回值:回调处理完的变化值
  • void RegisterPostCurrentValueChange(Action<AttributeBase, float, float> action)
    • 注册Attribute的当前值变化后回调
    • action:回调函数
      • AttributeBase:AttributeBase实例
      • float:变化前的值
      • float:变化后的实际值
  • void UnregisterPreBaseValueChange(Func<AttributeBase, float,float> func)
    • 注销Attribute的基础值变化前回调
    • func:注销的回调函数
  • void UnregisterPostBaseValueChange(Action<AttributeBase, float, float> action)
    • 注销Attribute的基础值变化后回调
    • action:注销的回调函数
  • void UnregisterPreCurrentValueChange(Func<AttributeBase, float, float> func)
    • 注销Attribute的当前值变化前回调
    • func:注销的回调函数
  • void UnregisterPostCurrentValueChange(Action<AttributeBase, float, float> action)
    • 注销Attribute的当前值变化后回调
    • action:注销的回调函数

3.4.3 AttributeAggregator

AttributeAggregator是Attribute的单位性质的聚合器,每个AttributeBase会对应一个AttributeAggregator。 AttributeAggregator是完全闭合独立运作,除了构造函数外不提供任何对外方法。 每当AttributeBase的BaseValue变化时,AttributeAggregator会自动更新自己的CurrentValue。

3.4.4 DerivedAttribute(W.I.P)

推导性质的Attribute,理论上不是一个类,而是一个Attribute的设计策略。


3.5 AttributeSet

3.5.1 AttributeSet

AttributeSet是一个抽象基类。

  • public abstract AttributeBase this[int index] { get; }
    • 通过AttributeBase的短名作为索引获取AttributeBase
  • public abstract string[] AttributeNames { get; }
    • AttributeSet的所有Attribute的短名
  • public void ChangeAttributeBase(string attributeShortName, float value)
    • 修改AttributeBase的基础值
    • attributeShortName:Attribute的短名
    • value:指定的值
3.5.1.a GAttrSetLib.gen( Script-Generated Code)

GAttrSetLib.gen是便于读取,管理AttributeSet工具脚本。 GAttrSetLib.gen不是EX-GAS框架内脚本的,需要EX-GAS框架AttributeSet配置改动后,通过生成脚本生成。

  • 脚本内包含如下静态工具类

  • public static class GAttrSetLib
    {
       public static readonly Dictionary<string,Type> AttrSetTypeDict = new Dictionary<string, Type>()
       {
          {"Fight",typeof(AS_Fight)},
       };
    
       public static List<string> AttributeFullNames=new List<string>()
       {
         "AS_Fight.HP",
         "AS_Fight.MP",
         "AS_Fight.STAMINA",
         "AS_Fight.POSTURE",
         "AS_Fight.ATK",
         "AS_Fight.SPEED",
       };
    }
    
  • AttrSetTypeDict:AttributeSet的类型字典,方便外部通过字符串名获取AttributeSet的类型。

  • AttributeFullNames:所有AttributeSet的所有Attribute的完整名

  • 举例:由脚本生成的AttributeSet类

public class AS_XXX:AttributeSet
{
    private AttributeBase _A = new AttributeBase("AS_XXX","A");
    public AttributeBase A => _A;
    public void InitA(float value)
    {
        _A.SetBaseValue(value);
        _A.SetCurrentValue(value);
    }
      public void SetCurrentA(float value)
    {
        _A.SetCurrentValue(value);
    }
      public void SetBaseA(float value)
    {
        _A.SetBaseValue(value);
    }
    
      public override AttributeBase this[string key]
      {
          get
          {
              switch (key)
              {
                 case "A":
                    return _A;
              }
              return null;
          }
      }

      public override string[] AttributeNames { get; } =
      {
          "A",
      };
}
  • 配置的AttributeSet名为XXX,包含一个Attribute名为A。

3.5.2 AttributeSetContainer

AttributeSetContainer是AttributeSet的容器类,用于ASC管理AttributeSet。

  • Dictionary<string,AttributeSet> Sets => _attributeSets; :AttributeSet的集合,为属性,只读。
  • void AddAttributeSet<T>() where T : AttributeSet:添加AttributeSet
    • T:指定的AttributeSet类
  • void AddAttributeSet(Type attrSetType):添加AttributeSet
    • attrSetType:指定的AttributeSet类型
  • bool TryGetAttributeSet<T>(out T attributeSet) where T : AttributeSet :尝试获取AttributeSet
    • attributeSet:获取的AttributeSet
    • 返回值:是否获取成功
  • float? GetAttributeBaseValue(string attrSetName,string attrShortName)
    • 获取指定Attribute的基础值
    • attrSetName:AttributeSet的名字
    • attrShortName:Attribute的短名
    • 返回值:Attribute的基础值
  • float? GetAttributeCurrentValue(string attrSetName,string attrShortName)
    • 获取指定Attribute的当前值
    • attrSetName:AttributeSet的名字
    • attrShortName:Attribute的短名
    • 返回值:Attribute的当前值
  • Dictionary<string, float> Snapshot()
    • 获取AttributeSetContainer的数据快照
    • 返回值:数据快照

3.5.3 CustomAttrSet

CustomAttrSet是AttributeSet的自定义类,适用于Runtime时动态生成AttributeSet。

  • void AddAttribute(AttributeBase attribute)
    • 添加Attribute
    • attribute:添加的Attribute
  • void RemoveAttribute(string attributeName)
    • 移除Attribute
    • attributeName:移除的Attribute的短名

3.6 GameplayEffect

3.6.1 GameplayEffectAsset

GameplayEffectAsset是GAS的游戏效配置类,是预设用ScriptableObject。

  • EffectsDurationPolicy DurationPolicy; :GameplayEffect的持续时间策略
  • float Duration :GameplayEffect的持续时间
  • float Period : GameplayEffect的周期
  • GameplayEffectAsset PeriodExecution :GameplayEffect的周期执行的GameplayEffect
  • GameplayEffectModifier[] Modifiers:GameplayEffect修改器
  • GameplayTag[] AssetTags :GameplayEffect的描述标签
  • GameplayTag[] GrantedTags :GameplayEffect的授予标签,GameplayEffect生效时会授予目标ASC这些标签,失效时会移除这些标签
  • GameplayTag[] ApplicationRequiredTags:GameplayEffect的应用要求标签,只有目标ASC持有【所有】这些标签时,GameplayEffect才会生效
  • GameplayTag[] OngoingRequiredTags: GameplayEffect的持续要求标签,只有目标ASC持有【所有】这些标签时,GameplayEffect才会持续生效
  • GameplayTag[] RemoveGameplayEffectsWithTags :GameplayEffect的移除标签,只要目标ASC的GameplayEffect持有【任意】这些标签时,这些GameplayEffect就会被移除
  • GameplayTag[] ApplicationImmunityTags:GameplayEffect的免疫标签,只要目标ASC持有【任意】这些标签时,这个GameplayEffect就不会生效
  • GameplayCueInstant[] CueOnExecute; :GameplayEffect执行时触发的GameplayCue
  • GameplayCueDurational[] CueDurational :GameplayEffect持续时触发的GameplayCue
  • GameplayCueInstant[] CueOnAdd:GameplayEffect添加时触发的GameplayCue
  • GameplayCueInstant[] CueOnRemove:GameplayEffect移除时触发的GameplayCue
  • GameplayCueInstant[] CueOnActivate:GameplayEffect激活时触发的GameplayCue
  • GameplayCueInstant[] CueOnDeactivate:GameplayEffect失效时触发的GameplayCue

3.6.2 GameplayEffect

GameplayEffect是GAS的Runtime的游戏效果数据类.运行游戏运行时动态生成GameplayEffect。

  • GameplayEffect的数据结构与GameplayEffectAsset几乎一致。这里就不再多赘述数据变量了。

3.6.3 GameplayEffectSpec

  • void Apply():应用游戏效果。
  • void DisApply():取消游戏效果的应用。
  • void Activate():激活游戏效果。
  • void Deactivate():停用游戏效果。
  • bool CanRunning():检查游戏效果是否可以运行。
  • void Tick():更新游戏效果的周期性行为。
  • void TriggerOnExecute():触发游戏效果执行时的事件。
  • void TriggerOnAdd():触发游戏效果添加时的事件。
  • void TriggerOnRemove():触发游戏效果移除时的事件。
  • void TriggerOnTick():触发游戏效果进行周期性更新时的事件。
  • void TriggerOnImmunity():触发游戏效果免疫时的事件。
  • void RemoveSelf():移除游戏效果自身。
  • void RegisterValue(GameplayTag tag, float value):注册与游戏标签关联的值。
    • tag:游戏标签。
    • value:与游戏标签关联的值。
  • void RegisterValue(string name, float value):注册与名称关联的值。
    • name:名称。
    • value:与名称关联的值。
  • bool UnregisterValue(GameplayTag tag):取消注册与游戏标签关联的值。
    • tag:游戏标签。
    • 返回值:如果成功取消注册,则返回 true,否则返回 false
  • bool UnregisterValue(string name):取消注册与名称关联的值。
    • name:名称。
    • 返回值:如果成功取消注册,则返回 true,否则返回 false
  • float? GetMapValue(GameplayTag tag):获取与游戏标签关联的值。
    • tag:游戏标签。
    • 返回值:如果找到与指定游戏标签关联的值,则返回该值;否则返回 null
  • float? GetMapValue(string name):获取与名称关联的值。
    • name:名称。
    • 返回值:如果找到与指定名称关联的值,则返回该值;否则返回 null

3.6.4 GameplayEffectContainer

GameplayEffectContainer是GameplayEffect的容器类,用于ASC管理GameplayEffect。

  • List<GameplayEffectSpec> GetActiveGameplayEffects():获取当前生效的游戏效果列表。
  • void Tick():处理所有生效游戏效果的周期性更新。
  • void RegisterOnGameplayEffectContainerIsDirty(Action action):注册效果容器变为脏状态时的回调函数。
    • action:回调函数。
  • void UnregisterOnGameplayEffectContainerIsDirty(Action action):取消注册效果容器变为脏状态时的回调函数。
    • action:回调函数。
  • void RemoveGameplayEffectWithAnyTags(GameplayTagSet tags):移除具有指定标签的游戏效果。
    • tags:指定的标签。
  • bool AddGameplayEffectSpec(GameplayEffectSpec spec):添加一个游戏效果实例。
    • spec:指定的游戏效果规范。
  • void RemoveGameplayEffectSpec(GameplayEffectSpec spec):移除指定的游戏效果实例。
    • spec:指定的游戏效果规范。
  • void RefreshGameplayEffectState():刷新游戏效果的状态,包括激活新效果和移除已停用的效果。
  • CooldownTimer CheckCooldownFromTags(GameplayTagSet tags):检查指定标签的冷却状态。
    • tags:指定的标签。
    • 返回值:冷却计时器。
  • void ClearGameplayEffect():清除所有游戏效果,包括移除已应用的效果和停用的效果。

3.6.5 CooldownTimer

CooldownTimer是冷却计时结构体,用于保存冷却时间数据。

  • public float TimeRemaining; : 剩余时间
  • public float Duration; : 总时间

3.6.6 GameplayEffectModifier

GameplayEffectModifier是游戏效果修改器类,用于实现对Attribute的修改。

  • string AttributeName:属性名称,用于标识游戏效果修改器所影响的属性。
  • float ModiferMagnitude:修改器的幅度值,用于指定属性修改的具体数值。
  • GEOperation Operation:修改器的操作类型,指定属性修改的方式,如增加、减少等。
  • ModifierMagnitudeCalculation MMC:修改器的计算方式,用于指定如何计算修改的幅度值。
  • void SetModiferMagnitude(float value):设置修改器的幅度值。
    • value:修改器的新幅度值。
  • void OnAttributeChanged():当属性名称发生变化时调用的方法,用于更新相关字段的值。
  • static void SetAttributeChoices():设置属性选择列表。
  • string AttributeSetName:属性集名称,用于标识游戏效果修改器所影响的属性集。
  • string AttributeShortName:属性短名称,用于标识游戏效果修改器所影响的属性的简短版本。
3.6.6.0 ModifierMagnitudeCalculation

ModifierMagnitudeCalculation是一个抽象基类,所有MMC必须继承自他。

  • public abstract float CalculateMagnitude(GameplayEffectSpec spec, AttributeBase attribute, float value):计算修改器的幅度值方法是MMC的根本。
    • spec:游戏效果规范。
    • attribute:属性基类。
    • value:指定的值。
    • 返回值:修改器的幅度值。
3.6.6.1 ScalableFloatModCalculation

ScalableFloatModCalculation是一个MMC的实现类,用于实现可缩放的浮点数修改器。

    public class ScalableFloatModCalculation:ModifierMagnitudeCalculation
    {
        [SerializeField] private float k;
        [SerializeField] private float b;

        public override float CalculateMagnitude(GameplayEffectSpec spec,float input)
        {
            return input * k + b;
        }
    }
  • float k:缩放系数。
  • float b:偏移量。
  • 执行逻辑:input * k + b。线性缩放。
3.6.6.2 AttributeBasedModCalculation

AttributeBasedModCalculation是一个MMC的实现类,用于实现基于属性的修改器。

    public class AttributeBasedModCalculation : ModifierMagnitudeCalculation
    {
        public enum AttributeFrom
        {
            Source,
            Target
        }

        public enum GEAttributeCaptureType
        {
            SnapShot,
            Track
        }

        public string attributeName;
        public string attributeSetName;
        public string attributeShortName;
        public AttributeFrom attributeFromType;
        public GEAttributeCaptureType captureType;
        public float k = 1;
        public float b = 0;

        public override float CalculateMagnitude(GameplayEffectSpec spec, float modifierMagnitude)
        {
            if (attributeFromType == AttributeFrom.Source)
            {
                if (captureType == GEAttributeCaptureType.SnapShot)
                {
                    var snapShot = spec.Source.DataSnapshot();
                    var attribute = snapShot[attributeName];
                    return attribute * k + b;
                }
                else
                {
                    var attribute = spec.Source.GetAttributeCurrentValue(attributeSetName, attributeShortName);
                    return (attribute ?? 1) * k + b;
                }
            }

            if (captureType == GEAttributeCaptureType.SnapShot)
            {
                var attribute = spec.Owner.DataSnapshot()[attributeName];
                return attribute * k + b;
            }
            else
            {
                var attribute = spec.Owner.GetAttributeCurrentValue(attributeSetName, attributeShortName);
                return (attribute ?? 1) * k + b;
            }
        }
    }
  • string attributeName:属性名称。
  • string attributeSetName:属性集名称。
  • string attributeShortName:属性短名称。
  • AttributeFrom attributeFromType:属性来源类型。
  • GEAttributeCaptureType captureType:游戏效果属性捕获类型。
  • float k:缩放系数。
  • float b:偏移量。
  • 执行逻辑:根据属性来源类型和游戏效果属性捕获类型,获取属性的当前值或快照值,并进行线性缩放。
3.6.6.3 SetByCallerFromNameModCalculation

SetByCallerFromNameModCalculation是一个MMC的实现类,用于实现根据名称设置的修改器。

    public class SetByCallerFromNameModCalculation : ModifierMagnitudeCalculation
    {
        [SerializeField] private string valueName;
        public override float CalculateMagnitude(GameplayEffectSpec spec,float input)
        {
            var value = spec.GetMapValue(valueName);
            return value ?? 0;
        }
    }
  • string valueName:键值值名称。
  • 执行逻辑:根据值名称获取与名称关联的值。
3.6.6.4 SetByCallerFromTagModCalculation

SetByCallerFromTagModCalculation是一个MMC的实现类,用于实现根据标签设置的修改器。

public class SetByCallerFromTagModCalculation:ModifierMagnitudeCalculation
    {
        [SerializeField] private GameplayTag _tag;
        public override float CalculateMagnitude(GameplayEffectSpec spec  ,float input)
        {
            var value = spec.GetMapValue(_tag);
            return value ?? 0;
        }
    }
  • GameplayTag _tag:键值标签。
  • 执行逻辑:根据游戏标签获取与游戏标签关联的值。

3.7 Ability

3.7.1 AbilityAsset

AbilityAsset是GAS的游戏能力配置类,是预设用ScriptableObject。他本身是一个抽象基类,所有的AbilityAsset都必须继承自他。

  • abstract Type AbilityType():能力的类型。用于把AbilityAsset和Ability类一一匹配。
    • 返回值:能力的类型。
  • string UniqueName:唯一名称,用于标识该能力。
  • GameplayEffectAsset Cost:花费效果,该能力的消耗效果。
  • GameplayEffectAsset Cooldown:冷却效果,该能力的冷却效果。如果为空,冷却时间也不会生效。
  • float CooldownTime:冷却时间,该能力的冷却时间长度。
  • GameplayTag[] AssetTag:资产标签,该能力的标签。
  • GameplayTag[] CancelAbilityTags:取消能力标签,用于取消该能力的标签。
  • GameplayTag[] BlockAbilityTags:阻止能力标签,用于阻止该能力的标签。
  • GameplayTag[] ActivationOwnedTag:激活所需标签,该能力激活所需的标签。
  • GameplayTag[] ActivationRequiredTags:激活要求标签,该能力激活所需的标签。
  • GameplayTag[] ActivationBlockedTags:激活阻止标签,用于阻止该能力的激活标签。

3.7.2 AbstractAbility

AbstractAbility是GAS的游戏能力数据基类,他本身是一个抽象基类,所有的Ability都必须继承自他。

  • string Name:名称,表示能力的名称。
  • AbilityAsset DataReference:数据引用,指向与该能力相关联的能力资产。
  • AbilityTagContainer Tag:标签,该能力的标签容器。
  • GameplayEffect Cooldown:冷却效果,该能力的冷却效果。
  • float CooldownTime:冷却时间,该能力的冷却时间长度。
  • GameplayEffect Cost:花费效果,该能力的消耗效果。
  • AbstractAbility(AbilityAsset abilityAsset):抽象能力构造函数,初始化抽象能力实例。
    • abilityAsset:能力资产,与该能力相关联的能力资产。
  • abstract AbilitySpec CreateSpec(AbilitySystemComponent owner):创建能力规格的抽象方法,用于生成能力的规格实例。
    • owner:所有者,拥有该能力的实体。
  • void SetCooldown(GameplayEffect coolDown):设置冷却效果的方法。
    • coolDown:冷却效果,要设置的冷却效果。
  • void SetCost(GameplayEffect cost):设置花费效果的方法。
    • cost:花费效果,要设置的花费效果。

3.7.2.a AbstractAbility :AbstractAbility where T : AbilityAsset

AbstractAbility是AbstractAbility的泛型子类,用于实现AbstractAbility的泛型版本。 通常Ability都继承自他。方便对应的AbilityAsset和Ability一一匹配。

3.7.3 AbilitySpec

AbilitySpec是GAS的游戏能力规格类,用于实现对Ability的实例化。本身是一个抽象基类,所有的AbilitySpec都必须继承自他。 AbilitySpec是用于实现Ability游戏内实际的表现逻辑。

  • AbstractAbility Ability:能力,与该能力规格类相关联的能力实例。
  • AbilitySystemComponent Owner:所有者,拥有该能力规格的单位。
  • float Level:等级,该能力的等级。
  • bool IsActive:是否激活,表示该能力当前是否处于激活状态。
  • int ActiveCount:激活计数,记录该能力被激活的次数。
  • void RegisterActivateResult(Action<AbilityActivateResult> onActivateResult):注册激活结果的方法,用于注册激活结果的回调函数。
  • void UnregisterActivateResult(Action<AbilityActivateResult> onActivateResult):注销激活结果的方法,用于注销激活结果的回调函数。
  • void RegisterEndAbility(Action onEndAbility):注册结束能力的方法,用于注册结束能力的回调函数。
  • void UnregisterEndAbility(Action onEndAbility):注销结束能力的方法,用于注销结束能力的回调函数。
  • void RegisterCancelAbility(Action onCancelAbility):注册取消能力的方法,用于注册取消能力的回调函数。
  • void UnregisterCancelAbility(Action onCancelAbility):注销取消能力的方法,用于注销取消能力的回调函数。
  • virtual AbilityActivateResult CanActivate():检查能力规格是否可以被激活。
    • 返回值:激活结果:
      • Success:成功
      • FailHasActivated:失败,已经激活
      • FailTagRequirement:失败,Tag要求不满足
      • FailCost: 失败,消耗不足
      • FailCooldown: 失败,还在冷却
      • FailOtherReason: 失败,其他原因
  • void DoCost():执行花费的方法,用于执行激活该能力规格的花费操作。
  • virtual bool TryActivateAbility(params object[] args):尝试激活能力
  • virtual void TryEndAbility():尝试结束能力
  • virtual void TryCancelAbility():尝试取消能力
  • void Tick():处理能力的帧更新。
  • abstract void ActivateAbility(params object[] args):激活能力的抽象方法,用于执行激活该能力的操作。
  • abstract void CancelAbility():取消能力的抽象方法,用于执行取消该能力的操作。
  • abstract void EndAbility():结束能力的抽象方法,用于执行结束该能力的操作。

3.7.4 AbilityContainer

能力容器,是ASC的间接管理能力的对象。

  • void Tick():处理的方法,用于处理能力容器中所有能力的Tick逻辑。
  • void GrantAbility(AbstractAbility ability):授予能力的方法,用于向能力容器中添加新的能力。
    • ability:能力,要添加的新能力实例。
  • void RemoveAbility(AbstractAbility ability):移除能力的方法,根据能力实例从能力容器中移除能力规格。
    • ability:能力,要移除的能力实例。
  • public void RemoveAbility(string abilityName):移除能力的方法,根据能力名称从能力容器中移除能力规格。
    • abilityName:能力名称,要移除的能力名称。
  • bool TryActivateAbility(string abilityName, params object[] args):尝试激活能力的方法
    • abilityName:能力名称,要激活的能力名称。
    • args:参数,激活能力所需的额外参数。
    • 返回值:布尔值,表示能否成功激活能力。
  • void EndAbility(string abilityName):结束能力
    • abilityName:能力名称,要结束的能力名称。
  • void CancelAbility(string abilityName):取消能力
    • abilityName:能力名称,要取消的能力名称。
  • Dictionary<string, AbilitySpec> AbilitySpecs():获取容器内所有能力字典
    • 返回值:包含所有能力规格的能力名称与对应的能力规格实例。

3.7.5 AbilityTask(W.I.P)

Ability我们只能控制他的激活,结束等,并且这些接口都是功能性的即时方法,不存在异步,持续管理的说法。

但是Ability不可能都是瞬时逻辑,因此在Ability的逻辑实现中需要开发者对Tick处理,或者使用异步自行实现逻辑。 而在UE的GAS中,为了解决这个问题,设计团队创造了AbilityTask的概念,他们让AbilityTask来承载实现Ability 逻辑的任务。在UE版本的GAS中,AbilityTask的种类很多,他们能实现即时/异步/持续/等待的逻辑处理。功能非常强大。

因此,我也试着模仿了这个概念,但目前的版本来说,AbilityTask的功能和目的性还很弱。在之后的版本迭代中,我会慢慢完善 AbilityTask,以此来强化GAS中的Ability的逻辑处理能力和可编辑性。

  • AbilityTaskBase:基类,Task是依附于Ability的存在,因此他的初始化必须依赖于AbilitySpec。
    • public abstract class AbilityTaskBase
      {
          protected AbilitySpec _spec;
          public AbilitySpec Spec => _spec;
          public virtual void Init(AbilitySpec spec)
          {
              _spec = spec;
          }
      }
      
  • InstantAbilityTask: 即时类型的Task,最为常见的Task之一。
    • public abstract class InstantAbilityTask:AbilityTaskBase
      {
          #if UNITY_EDITOR
          /// <summary>
          ///  编辑器预览用
          ///  【注意】 覆写时,记得用UNITY_EDITOR宏包裹,这是预览表现用的函数,不该被编译。
          /// </summary>
          public virtual void OnEditorPreview()
          {
          }
          #endif
          public abstract void OnExecute();
      }
      
  • OngoingAbilityTask: 持续类型的Task,目前这类Task和TimelineAbility强关联,往后的设计里会抽象出来,让Task更加灵活。
    • public abstract class OngoingAbilityTask:AbilityTaskBase
      {
          #if UNITY_EDITOR
          /// <summary>
          /// 编辑器预览用
          /// 【注意】 覆写时,记得用UNITY_EDITOR宏包裹,这是预览表现用的函数,不该被编译。
          /// </summary>
          /// <param name="frame"></param>
          /// <param name="startFrame"></param>
          /// <param name="endFrame"></param>
          public virtual void OnEditorPreview(int frame, int startFrame, int endFrame)
          {
          }
          #endif
          public abstract void OnStart(int startFrame);
      
          public abstract void OnEnd(int endFrame);
      
          public abstract void OnTick(int frameIndex,int startFrame,int endFrame);
      }
      


3.8 GameplayCue

3.8.1 GameplayCue

GameplayCue是GAS的游戏提示配置类,用于实现对游戏效果的提示。他本身是一个抽象基类,所有的GameplayCue都必须继承自他。

  • GameplayTag[] RequiredTags; :GameplayCue的要求标签,持有【所有】RequiredTags才可触发
  • GameplayTag[] ImmunityTags; :GameplayCue的免疫标签,持有【任意】ImmunityTags不可触发
3.8.1.a public abstract class GameplayCue : GameplayCue where T : GameplayCueSpec

这个泛型类是为了方便对应的GameplayCueSpec和GameplayCue一一匹配,方便使用。

3.8.2 GameplayCueSpec

GameplayCueSpec是GAS的游戏提示规格类,用于实现对GameplayCue的实例化。本身是一个抽象基类,所有的GameplayCueSpec都必须继承自他。 GameplayCueSpec内实现GameplayCue游戏内实际的表现逻辑。

        public virtual bool Triggerable()
        {
            return _cue.Triggerable(Owner);
        }
  • Triggerable():检查是否可以触发游戏提示的方法。

3.8.3 GameplayCueParameters

GameplayCueParameters是GAS的游戏提示参数结构体,用于实现对GameplayCue的参数化。 目前逻辑简单粗暴,存在拆装箱过程。

    public struct GameplayCueParameters
    {
        public GameplayEffectSpec sourceGameplayEffectSpec; 
        public AbilitySpec sourceAbilitySpec;
        public object[] customArguments;
    }

3.8.4 GameplayCueInstant

GameplayCueInstant是GAS的GameplayCue中的一大类,属于OneShot类型的Cue。

3.8.4.a GameplayCueInstant
  • InstantCueApplyTarget applyTarget:立即提示应用目标,指示立即提示的应用目标类型。
  • virtual void ApplyFrom(GameplayEffectSpec gameplayEffectSpec):从GameplayEffectSpec应用InstantCue。
    • gameplayEffectSpec:游戏效果规格,触发立即提示的游戏效果规格实例。
  • virtual void ApplyFrom(AbilitySpec abilitySpec, params object[] customArguments):从AbilitySpec应用InstantCue。
    • abilitySpec:能力规格,触发立即提示的能力规格实例。
    • customArguments:自定义参数,自定义参数数组。
3.8.4.b GameplayCueInstantSpec

GameplayCueInstantSpec必须覆写Trigger()方法,用于实现对GameplayCueInstant触发。

public abstract class GameplayCueInstantSpec : GameplayCueSpec
    {
        public GameplayCueInstantSpec(GameplayCueInstant cue, GameplayCueParameters parameters) : base(cue,
            parameters)
        {
        }
        
        public abstract void Trigger();
    }

3.8.5 GameplayCueDuration

GameplayCueDuration是GAS的GameplayCue中的一大类,属于持续类型的Cue。

3.8.5.a GameplayCueDurational
  • public GameplayCueDurationalSpec ApplyFrom(GameplayEffectSpec gameplayEffectSpec): 从GameplayEffectSpec应用DurationalCue。
    • gameplayEffectSpec:游戏效果规格,触发持续提示的游戏效果规格实例。
  • public GameplayCueDurationalSpec ApplyFrom(AbilitySpec abilitySpec, params object[] customArguments): 从AbilitySpec应用DurationalCue。
    • abilitySpec:能力规格,触发持续提示的能力规格实例。
    • customArguments:自定义参数,自定义参数数组。
3.8.5.b GameplayCueDurationalSpec

GameplayCueDurationalSpec必须覆写 OnAdd(), OnRemove(), OnGameplayEffectActivate(), OnGameplayEffectDeactivate(), OnTick()方法, 用于实现对GameplayCueDurational触发和运作。

    public abstract class GameplayCueDurationalSpec : GameplayCueSpec
    {
        protected GameplayCueDurationalSpec(GameplayCueDurational cue, GameplayCueParameters parameters) : 
            base(cue, parameters)
        {
        }

        public abstract void OnAdd();
        public abstract void OnRemove();
        public abstract void OnGameplayEffectActivate();
        public abstract void OnGameplayEffectDeactivate();
        public abstract void OnTick();
    }

4.可视化功能

1. GAS Setting Manager (GAS基础配置管理器)

QQ20240313174500.png 基础配置是与项目工程唯一对应的,所以入口放在了ProjectSetting,另外还有Edit Menu栏入口:EX-GAS -> Setting

  • GameplayTag Manager QQ20240313114652.png
  • Attribute Manager QQ20240313115953.png
  • AttributeSet Manager QQ20240313121300.png

2. GAS Asset Aggregator (GAS配置资源聚合器)

QQ20240313175247.png 因为GAS使用过程需要大量的配置(各类预设:ASC,游戏能力,游戏效果/buff,游戏提示,MMC),为了方便集中管理,我制作了一个配置资源聚合器。

通过在菜单栏EX-GAS -> Asset Aggregator 可以打开配置资源聚合器。

聚合器支持:分类管理,文件夹树结构显示,搜索栏快速查找,快速创建/删除配置文件(右上角的快捷按钮)

  • ASC预设管理 QQ20240313175513.png
  • 能力配置管理 QQ20240313175749.png
  • 游戏效果管理 QQ20240313175829.png
  • 游戏提示 & MMC 管理 QQ20240313180028.png QQ20240313180054.png

3. GAS Runtime Watcher (GAS运行时监视器)

QQ20240313180923.png 注意!由于该监视器的监视刷新逻辑过于暴力,因此存在明显的性能问题。监视器只是为了方便调试,所以建议不要一直后台挂着监视器,有需要时再打开。

目前监视器较为简陋,以后可能会优化监视器。


5.如果...我想...,应该怎么做?(W.I.P)

  • Q:我想实现血量(HpMax)上限,随力量(STR)每增加1点,血量上限增加10%,怎么办?
    • A: 有两种常见的方法:
        1. 采用Derived Attribute的设计方法,给单位添加一个Infinite的GameplayEffect, 在修改器参数列表中添加一个修改器:修改属性为HpMax;操作类型为乘法;模值随意; MMC为STR属性依赖的MMC,来源为Target,并且Capture必须为Track(只有为Track时才能触发实时重计算),剩下的线性参数为k=0.1,b=1 (Magnitude = 1 + 10% * STR )。
        1. 采用监听STR属性的变化事件,手动对HpMax的BaseValue进行修改同步。

6.暂不支持的功能(可能有遗漏)

  • RPC相关的GE复制广播
  • GameplayEffect Execution,目前只有Modifier,没有Execution
  • Ability的触发判断用的Source/Target Tag目前不生效
  • GE过期时,触发的游戏效果

7.后续计划

  • 修复bug ,性能优化
  • 将GAS采用ECS结构来运行
  • 补全遗漏的功能
  • 优化Ability的编辑
  • 支持RPC的GE复制广播,网络同步

8.特别感谢

本插件全面参考了UE的GAS解析,来自github --@tranek

同时还有中译版本,来自github --@BillEliot

没有上述二位的文章,本项目的开发会非常痛苦。

另外还要感谢开源项目:UnityToolchainsTrick

多亏UnityToolchainsTrick中的大量Editor开发技巧,极大的缩减了项目中编辑器的制作时间,省了很多事儿。非常感谢!

感谢参与EX-GAS开发的朋友们:

  • BBC :优化了很多编辑器的体验以及bug,还提出了很多问题和反馈。

9.插件反馈渠道

QQ群号:616570103

目前该插件是一定有大量bug存在的,因为有非常多的细节没有测试到,虽然有Demo演示,但也只是一部分的功能。所以我希望有人能使用该插件,多多反馈,来完善该插件。

GAS使用门槛高,所以有任何GAS相关使用的疑问,bug或者建议,欢迎来反馈群里交流。我都会尽可能回答的。